Skip to content

Commit 856bf3e

Browse files
committed
Try to serialize lists/dicts as structpb
1 parent fdbedc1 commit 856bf3e

File tree

2 files changed

+88
-0
lines changed

2 files changed

+88
-0
lines changed

src/dispatch/any.py

+73
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import google.protobuf.duration_pb2
99
import google.protobuf.empty_pb2
1010
import google.protobuf.message
11+
import google.protobuf.struct_pb2
1112
import google.protobuf.timestamp_pb2
1213
import google.protobuf.wrappers_pb2
1314
from google.protobuf import descriptor_pool, message_factory
@@ -44,6 +45,12 @@ def marshal_any(value: Any) -> google.protobuf.any_pb2.Any:
4445
nanos = value.microseconds * 1000
4546
value = google.protobuf.duration_pb2.Duration(seconds=seconds, nanos=nanos)
4647

48+
if isinstance(value, list) or isinstance(value, dict):
49+
try:
50+
value = as_struct_value(value)
51+
except ValueError:
52+
pass # fallthrough
53+
4754
if not isinstance(value, google.protobuf.message.Message):
4855
value = pickled_pb.Pickled(pickled_value=pickle.dumps(value))
4956

@@ -64,34 +71,100 @@ def unmarshal_any(any: google.protobuf.any_pb2.Any) -> Any:
6471

6572
if isinstance(proto, pickled_pb.Pickled):
6673
return pickle.loads(proto.pickled_value)
74+
6775
elif isinstance(proto, google.protobuf.empty_pb2.Empty):
6876
return None
77+
6978
elif isinstance(proto, google.protobuf.wrappers_pb2.BoolValue):
7079
return proto.value
80+
7181
elif isinstance(proto, google.protobuf.wrappers_pb2.Int32Value):
7282
return proto.value
83+
7384
elif isinstance(proto, google.protobuf.wrappers_pb2.Int64Value):
7485
return proto.value
86+
7587
elif isinstance(proto, google.protobuf.wrappers_pb2.UInt32Value):
7688
return proto.value
89+
7790
elif isinstance(proto, google.protobuf.wrappers_pb2.UInt64Value):
7891
return proto.value
92+
7993
elif isinstance(proto, google.protobuf.wrappers_pb2.FloatValue):
8094
return proto.value
95+
8196
elif isinstance(proto, google.protobuf.wrappers_pb2.DoubleValue):
8297
return proto.value
98+
8399
elif isinstance(proto, google.protobuf.wrappers_pb2.StringValue):
84100
return proto.value
101+
85102
elif isinstance(proto, google.protobuf.wrappers_pb2.BytesValue):
86103
try:
87104
# Assume it's the legacy container for pickled values.
88105
return pickle.loads(proto.value)
89106
except Exception as e:
90107
# Otherwise, return the literal bytes.
91108
return proto.value
109+
92110
elif isinstance(proto, google.protobuf.timestamp_pb2.Timestamp):
93111
return proto.ToDatetime(tzinfo=UTC)
112+
94113
elif isinstance(proto, google.protobuf.duration_pb2.Duration):
95114
return proto.ToTimedelta()
96115

116+
elif isinstance(proto, google.protobuf.struct_pb2.Value):
117+
return from_struct_value(proto)
118+
97119
return proto
120+
121+
122+
def as_struct_value(value: Any) -> google.protobuf.struct_pb2.Value:
123+
if value is None:
124+
null_value = google.protobuf.struct_pb2.NullValue.NULL_VALUE
125+
return google.protobuf.struct_pb2.Value(null_value=null_value)
126+
127+
elif isinstance(value, bool):
128+
return google.protobuf.struct_pb2.Value(bool_value=value)
129+
130+
elif isinstance(value, int) or isinstance(value, float):
131+
return google.protobuf.struct_pb2.Value(number_value=float(value))
132+
133+
elif isinstance(value, str):
134+
return google.protobuf.struct_pb2.Value(string_value=value)
135+
136+
elif isinstance(value, list):
137+
list_value = google.protobuf.struct_pb2.ListValue(
138+
values=[as_struct_value(v) for v in value]
139+
)
140+
return google.protobuf.struct_pb2.Value(list_value=list_value)
141+
142+
elif isinstance(value, dict):
143+
for key in value.keys():
144+
if not isinstance(key, str):
145+
raise ValueError("unsupported object key")
146+
147+
struct_value = google.protobuf.struct_pb2.Struct(
148+
fields={k: as_struct_value(v) for k, v in value.items()}
149+
)
150+
return google.protobuf.struct_pb2.Value(struct_value=struct_value)
151+
152+
raise ValueError("unsupported value")
153+
154+
155+
def from_struct_value(value: google.protobuf.struct_pb2.Value) -> Any:
156+
if value.HasField("null_value"):
157+
return None
158+
elif value.HasField("bool_value"):
159+
return value.bool_value
160+
elif value.HasField("number_value"):
161+
return value.number_value
162+
elif value.HasField("string_value"):
163+
return value.string_value
164+
elif value.HasField("list_value"):
165+
166+
return [from_struct_value(v) for v in value.list_value.values]
167+
elif value.HasField("struct_value"):
168+
return {k: from_struct_value(v) for k, v in value.struct_value.fields.items()}
169+
else:
170+
raise RuntimeError(f"invalid struct_pb2.Value: {value}")

tests/dispatch/test_any.py

+15
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,18 @@ def test_unmarshal_protobuf_message():
9494
)
9595

9696
assert message == unmarshal_any(boxed)
97+
98+
99+
def test_unmarshal_json_like():
100+
value = {
101+
"null": None,
102+
"bool": True,
103+
"int": 11,
104+
"float": 3.14,
105+
"string": "foo",
106+
"list": [None, "abc", 1.23],
107+
"object": {"a": ["b", "c"]},
108+
}
109+
boxed = marshal_any(value)
110+
assert "type.googleapis.com/google.protobuf.Value" == boxed.type_url
111+
assert value == unmarshal_any(boxed)

0 commit comments

Comments
 (0)