Skip to content

Commit 0ac297c

Browse files
committed
Update types and handle data_base64 structured.
- Add sane defaults for encoding - Unfortunately, defaults for structured and binary need to be *different* - Push types through interfaces - Make it easy to call 'ToRequest' using Marshaller defaults - Add tests for above
1 parent cda44dd commit 0ac297c

7 files changed

Lines changed: 108 additions & 41 deletions

File tree

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def run_binary(event, url):
129129
print("binary CloudEvent")
130130
for k, v in binary_headers.items():
131131
print("{0}: {1}\r\n".format(k, v))
132-
print(binary_data.getvalue())
132+
print(binary_data)
133133
response = requests.post(url,
134134
headers=binary_headers,
135135
data=binary_data.getvalue())
@@ -138,14 +138,14 @@ def run_binary(event, url):
138138

139139
def run_structured(event, url):
140140
structured_headers, structured_data = http_marshaller.ToRequest(
141-
event, converters.TypeStructured, json.dumps
142-
)
141+
event, converters.TypeStructured, json.dumps)
142+
143143
print("structured CloudEvent")
144-
print(structured_data.getvalue())
144+
print(structured_data.decode("utf-8"))
145145

146146
response = requests.post(url,
147147
headers=structured_headers,
148-
data=structured_data.getvalue())
148+
data=structured_data)
149149
response.raise_for_status()
150150

151151
```
@@ -163,7 +163,7 @@ The code below shows how integrate both libraries in order to create a CloudEven
163163
event = v02.Event()
164164
http_marshaller = marshaller.NewDefaultHTTPMarshaller()
165165
event = http_marshaller.FromRequest(
166-
event, headers, data, json.load)
166+
event, headers, data)
167167

168168
```
169169
Complete example of turning a CloudEvent into a request you can find [here](samples/python-requests/request_to_cloudevent.py).

cloudevents/sdk/converters/binary.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,16 @@ def read(
3636
event: event_base.BaseEvent,
3737
headers: dict,
3838
body: typing.IO,
39-
data_unmarshaller: typing.Callable,
39+
data_unmarshaller: event_base.UnmarshallerType,
4040
) -> event_base.BaseEvent:
4141
if type(event) not in self.SUPPORTED_VERSIONS:
4242
raise exceptions.UnsupportedEvent(type(event))
4343
event.UnmarshalBinary(headers, body, data_unmarshaller)
4444
return event
4545

4646
def write(
47-
self, event: event_base.BaseEvent, data_marshaller: typing.Callable
48-
) -> (dict, typing.IO):
47+
self, event: event_base.BaseEvent, data_marshaller: event_base.MarshallerType
48+
) -> (dict, bytes):
4949
return event.MarshalBinary(data_marshaller)
5050

5151

cloudevents/sdk/converters/structured.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,16 @@ def read(
3535
event: event_base.BaseEvent,
3636
headers: dict,
3737
body: typing.IO,
38-
data_unmarshaller: typing.Callable,
38+
data_unmarshaller: event_base.UnmarshallerType,
3939
) -> event_base.BaseEvent:
4040
event.UnmarshalJSON(body, data_unmarshaller)
4141
return event
4242

4343
def write(
44-
self, event: event_base.BaseEvent, data_marshaller: typing.Callable
45-
) -> (dict, typing.IO):
44+
self, event: event_base.BaseEvent, data_marshaller: event_base.UnmarshallerType
45+
) -> (dict, bytes):
4646
http_headers = {"content-type": self.MIME_TYPE}
47-
return http_headers, event.MarshalJSON(data_marshaller)
47+
return http_headers, event.MarshalJSON(data_marshaller).encode("utf-8")
4848

4949

5050
def NewJSONHTTPCloudEventConverter() -> JSONHTTPCloudEventConverter:

cloudevents/sdk/event/base.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,16 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15+
import base64
1516
import io
1617
import json
1718
import typing
1819

20+
# Use consistent types for marshal and unmarshal functions across
21+
# both JSON and Binary format.
22+
MarshallerType = typing.Callable[[typing.Any], typing.Union[bytes, str]]
23+
UnmarshallerType = typing.Callable[[typing.IO], typing.Any]
24+
1925

2026
# TODO(slinkydeveloper) is this really needed?
2127
class EventGetterSetter(object):
@@ -110,23 +116,39 @@ def Set(self, key: str, value: object):
110116
exts.update({key: value})
111117
self.Set("extensions", exts)
112118

113-
def MarshalJSON(self, data_marshaller: typing.Callable) -> typing.IO:
119+
def MarshalJSON(self, data_marshaller: MarshallerType) -> str:
120+
if data_marshaller is None:
121+
data_marshaller = lambda x: x
114122
props = self.Properties()
115-
props["data"] = data_marshaller(props.get("data"))
116-
return io.BytesIO(json.dumps(props).encode("utf-8"))
117-
118-
def UnmarshalJSON(self, b: typing.IO, data_unmarshaller: typing.Callable):
123+
data = ""
124+
if "data" in props:
125+
data = data_marshaller(props.get("data"))
126+
del props["data"]
127+
if isinstance(data, bytes):
128+
props["data_base64"] = base64.b64encode(data).decode("ascii")
129+
else:
130+
props["data"] = data
131+
return json.dumps(props)
132+
133+
def UnmarshalJSON(
134+
self,
135+
b: typing.IO,
136+
data_unmarshaller: UnmarshallerType
137+
):
119138
raw_ce = json.load(b)
120139
for name, value in raw_ce.items():
121140
if name == "data":
122-
value = data_unmarshaller(value)
141+
value = data_unmarshaller(io.StringIO(value))
142+
if name == "data_base64":
143+
value = data_unmarshaller(io.BytesIO(base64.b64decode(value)))
144+
name = "data"
123145
self.Set(name, value)
124146

125147
def UnmarshalBinary(
126148
self,
127149
headers: dict,
128150
body: typing.IO,
129-
data_unmarshaller: typing.Callable
151+
data_unmarshaller: UnmarshallerType
130152
):
131153
for header, value in headers.items():
132154
header = header.lower()
@@ -139,8 +161,10 @@ def UnmarshalBinary(
139161

140162
def MarshalBinary(
141163
self,
142-
data_marshaller: typing.Callable
143-
) -> (dict, object):
164+
data_marshaller: MarshallerType
165+
) -> (dict, bytes):
166+
if data_marshaller is None:
167+
data_marshaller = json.dumps
144168
headers = {}
145169
if self.ContentType():
146170
headers["content-type"] = self.ContentType()
@@ -154,4 +178,7 @@ def MarshalBinary(
154178
headers["ce-{0}".format(key)] = value
155179

156180
data, _ = self.Get("data")
157-
return headers, data_marshaller(data)
181+
data = data_marshaller(data)
182+
if isinstance(data, str): # Convenience method for json.dumps
183+
data = data.encode("utf-8")
184+
return headers, data

cloudevents/sdk/marshaller.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# under the License.
1414

1515
import typing
16+
import json
1617

1718
from cloudevents.sdk import exceptions
1819

@@ -43,7 +44,7 @@ def FromRequest(
4344
event: event_base.BaseEvent,
4445
headers: dict,
4546
body: typing.IO,
46-
data_unmarshaller: typing.Callable,
47+
data_unmarshaller: event_base.UnmarshallerType = json.load,
4748
) -> event_base.BaseEvent:
4849
"""
4950
Reads a CloudEvent from an HTTP headers and request body
@@ -78,9 +79,9 @@ def FromRequest(
7879
def ToRequest(
7980
self,
8081
event: event_base.BaseEvent,
81-
converter_type: str,
82-
data_marshaller: typing.Callable,
83-
) -> (dict, typing.IO):
82+
converter_type: str = None,
83+
data_marshaller: event_base.MarshallerType = None,
84+
) -> (dict, bytes):
8485
"""
8586
Writes a CloudEvent into a HTTP-ready form of headers and request body
8687
:param event: CloudEvent
@@ -92,9 +93,12 @@ def ToRequest(
9293
:return: dict of HTTP headers and stream of HTTP request body
9394
:rtype: tuple
9495
"""
95-
if not isinstance(data_marshaller, typing.Callable):
96+
if data_marshaller is not None and not isinstance(data_marshaller, typing.Callable):
9697
raise exceptions.InvalidDataMarshaller()
9798

99+
if converter_type is None:
100+
converter_type = self.__converters[0].TYPE
101+
98102
if converter_type in self.__converters_by_type:
99103
cnvrtr = self.__converters_by_type[converter_type]
100104
return cnvrtr.write(event, data_marshaller)

cloudevents/tests/test_event_from_request_converter.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
def test_binary_converter_upstream(event_class):
3535
m = marshaller.NewHTTPMarshaller(
3636
[binary.NewBinaryHTTPCloudEventConverter()])
37-
event = m.FromRequest(event_class(), data.headers[event_class], None, lambda x: x)
37+
event = m.FromRequest(
38+
event_class(), data.headers[event_class], None, lambda x: x)
3839
assert event is not None
3940
assert event.EventType() == data.ce_type
4041
assert event.EventID() == data.ce_id
@@ -100,6 +101,7 @@ def test_structured_converter_v01():
100101
assert event.Get("type") == (data.ce_type, True)
101102
assert event.Get("id") == (data.ce_id, True)
102103

104+
103105
@pytest.mark.parametrize("event_class", [v02.Event, v03.Event, v1.Event])
104106
def test_default_http_marshaller_with_structured(event_class):
105107
m = marshaller.NewDefaultHTTPMarshaller()
@@ -147,15 +149,11 @@ def test_unsupported_event_configuration():
147149

148150
def test_invalid_data_unmarshaller():
149151
m = marshaller.NewDefaultHTTPMarshaller()
150-
pytest.raises(
151-
exceptions.InvalidDataUnmarshaller,
152-
m.FromRequest,
153-
v01.Event(), {}, None, None
154-
)
152+
with pytest.raises(exceptions.InvalidDataUnmarshaller):
153+
m.FromRequest(v01.Event(), {}, None, "string")
155154

156155

157156
def test_invalid_data_marshaller():
158157
m = marshaller.NewDefaultHTTPMarshaller()
159-
pytest.raises(
160-
exceptions.InvalidDataMarshaller, m.ToRequest, v01.Event(), "blah", None
161-
)
158+
with pytest.raises(exceptions.InvalidDataMarshaller):
159+
m.ToRequest(v01.Event(), "blah", "string")

cloudevents/tests/test_event_pipeline.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ def test_event_pipeline_upstream(event_class):
4545
assert "ce-id" in new_headers
4646
assert "ce-time" in new_headers
4747
assert "content-type" in new_headers
48-
assert isinstance(body, str)
49-
assert data.body == body
48+
assert isinstance(body, bytes)
49+
assert data.body == body.decode("utf-8")
5050

5151

5252
def test_extensions_are_set_upstream():
@@ -76,8 +76,8 @@ def test_event_pipeline_v01():
7676
m = marshaller.NewHTTPMarshaller([structured.NewJSONHTTPCloudEventConverter()])
7777

7878
_, body = m.ToRequest(event, converters.TypeStructured, lambda x: x)
79-
assert isinstance(body, io.BytesIO)
80-
new_headers = json.load(io.TextIOWrapper(body, encoding="utf-8"))
79+
assert isinstance(body, bytes)
80+
new_headers = json.loads(body)
8181
assert new_headers is not None
8282
assert "cloudEventsVersion" in new_headers
8383
assert "eventType" in new_headers
@@ -86,3 +86,41 @@ def test_event_pipeline_v01():
8686
assert "eventTime" in new_headers
8787
assert "contentType" in new_headers
8888
assert data.body == new_headers["data"]
89+
90+
def test_binary_event_v1():
91+
event = (
92+
v1.Event()
93+
.SetContentType("application/octet-stream")
94+
.SetData(b'\x00\x01')
95+
)
96+
m = marshaller.NewHTTPMarshaller([structured.NewJSONHTTPCloudEventConverter()])
97+
98+
_, body = m.ToRequest(event, converters.TypeStructured, lambda x: x)
99+
assert isinstance(body, bytes)
100+
content = json.loads(body)
101+
assert "data" not in content
102+
assert content["data_base64"] == "AAE=", f"Content is: {content}"
103+
104+
def test_object_event_v1():
105+
event = (
106+
v1.Event()
107+
.SetContentType("application/json")
108+
.SetData({"name": "john"})
109+
)
110+
111+
m = marshaller.NewDefaultHTTPMarshaller()
112+
113+
_, structuredBody = m.ToRequest(event)
114+
assert isinstance(structuredBody, bytes)
115+
structuredObj = json.loads(structuredBody)
116+
errorMsg = f"Body was {structuredBody}, obj is {structuredObj}"
117+
assert isinstance(structuredObj, dict), errorMsg
118+
assert isinstance(structuredObj["data"], dict), errorMsg
119+
assert len(structuredObj["data"]) == 1, errorMsg
120+
assert structuredObj["data"]["name"] == "john", errorMsg
121+
122+
headers, binaryBody = m.ToRequest(event, converters.TypeBinary)
123+
assert isinstance(headers, dict)
124+
assert isinstance(binaryBody, bytes)
125+
assert headers["content-type"] == "application/json"
126+
assert binaryBody == b'{"name": "john"}', f"Binary is {binaryBody!r}"

0 commit comments

Comments
 (0)