Skip to content

Commit 94c7c9a

Browse files
authored
Merge pull request #10 from cloudevents/0.2-force-improvements
0.2 force improvements
2 parents f4a3c05 + c9eec27 commit 94c7c9a

13 files changed

+350
-55
lines changed

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,69 @@ headers, body = m.ToRequest(event, converters.TypeStructured, lambda x: x)
7070

7171
```
7272

73+
## HOWTOs with various Python HTTP frameworks
74+
75+
In this topic you'd find various example how to integrate an SDK with various HTTP frameworks.
76+
77+
### Python requests
78+
79+
One of popular framework is [0.2-force-improvements](http://docs.python-requests.org/en/master/).
80+
81+
82+
#### CloudEvent to request
83+
84+
The code below shows how integrate both libraries in order to convert a CloudEvent into an HTTP request:
85+
86+
```python
87+
def run_binary(event, url):
88+
binary_headers, binary_data = http_marshaller.ToRequest(
89+
event, converters.TypeBinary, json.dumps)
90+
91+
print("binary CloudEvent")
92+
for k, v in binary_headers.items():
93+
print("{0}: {1}\r\n".format(k, v))
94+
print(binary_data.getvalue())
95+
response = requests.post(url,
96+
headers=binary_headers,
97+
data=binary_data.getvalue())
98+
response.raise_for_status()
99+
100+
101+
def run_structured(event, url):
102+
structured_headers, structured_data = http_marshaller.ToRequest(
103+
event, converters.TypeStructured, json.dumps
104+
)
105+
print("structured CloudEvent")
106+
print(structured_data.getvalue())
107+
108+
response = requests.post(url,
109+
headers=structured_headers,
110+
data=structured_data.getvalue())
111+
response.raise_for_status()
112+
113+
```
114+
115+
Complete example of turning a CloudEvent into a request you can find [here](samples/python-requests/cloudevent_to_request.py).
116+
117+
#### Request to CloudEvent
118+
119+
The code below shows how integrate both libraries in order to create a CloudEvent from an HTTP request:
120+
```python
121+
response = requests.get(url)
122+
response.raise_for_status()
123+
headers = response.headers
124+
data = io.BytesIO(response.content)
125+
event = v02.Event()
126+
http_marshaller = marshaller.NewDefaultHTTPMarshaller()
127+
event = http_marshaller.FromRequest(
128+
event, headers, data, json.load)
129+
130+
```
131+
Complete example of turning a CloudEvent into a request you can find [here](samples/python-requests/request_to_cloudevent.py).
132+
133+
134+
## SDK versioning
135+
73136
The goal of this package is to provide support for all released versions of CloudEvents, ideally while maintaining
74137
the same API. It will use semantic versioning with following rules:
75138
* MAJOR version increments when backwards incompatible changes is introduced.

cloudevents/sdk/converters/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ def read(self, event, headers: dict, body: typing.IO,
2525
data_unmarshaller: typing.Callable) -> base.BaseEvent:
2626
raise Exception("not implemented")
2727

28+
def event_supported(self, event: object) -> bool:
29+
raise Exception("not implemented")
30+
31+
def can_read(self, content_type: str) -> bool:
32+
raise Exception("not implemented")
33+
2834
def write(self, event: base.BaseEvent,
2935
data_marshaller: typing.Callable) -> (dict, typing.IO):
3036
raise Exception("not implemented")

cloudevents/sdk/converters/binary.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ class BinaryHTTPCloudEventConverter(base.Converter):
2525
TYPE = "binary"
2626
SUPPORTED_VERSIONS = [v02.Event, ]
2727

28+
def can_read(self, content_type: str) -> bool:
29+
return True
30+
31+
def event_supported(self, event: object) -> bool:
32+
return type(event) in self.SUPPORTED_VERSIONS
33+
2834
def read(self,
2935
event: event_base.BaseEvent,
3036
headers: dict, body: typing.IO,
@@ -36,11 +42,7 @@ def read(self,
3642

3743
def write(self, event: event_base.BaseEvent,
3844
data_marshaller: typing.Callable) -> (dict, typing.IO):
39-
if not isinstance(data_marshaller, typing.Callable):
40-
raise exceptions.InvalidDataMarshaller()
41-
42-
hs, data = event.MarshalBinary()
43-
return hs, data_marshaller(data)
45+
return event.MarshalBinary(data_marshaller)
4446

4547

4648
def NewBinaryHTTPCloudEventConverter() -> BinaryHTTPCloudEventConverter:

cloudevents/sdk/converters/structured.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,21 @@
1414

1515
import typing
1616

17-
from cloudevents.sdk import exceptions
1817
from cloudevents.sdk.converters import base
1918
from cloudevents.sdk.event import base as event_base
2019

2120

2221
class JSONHTTPCloudEventConverter(base.Converter):
2322

2423
TYPE = "structured"
24+
MIME_TYPE = "application/cloudevents+json"
25+
26+
def can_read(self, content_type: str) -> bool:
27+
return content_type and content_type.startswith(self.MIME_TYPE)
28+
29+
def event_supported(self, event: object) -> bool:
30+
# structured format supported by both spec 0.1 and 0.2
31+
return True
2532

2633
def read(self, event: event_base.BaseEvent,
2734
headers: dict,
@@ -33,10 +40,8 @@ def read(self, event: event_base.BaseEvent,
3340
def write(self,
3441
event: event_base.BaseEvent,
3542
data_marshaller: typing.Callable) -> (dict, typing.IO):
36-
if not isinstance(data_marshaller, typing.Callable):
37-
raise exceptions.InvalidDataMarshaller()
38-
39-
return {}, event.MarshalJSON(data_marshaller)
43+
http_headers = {'content-type': self.MIME_TYPE}
44+
return http_headers, event.MarshalJSON(data_marshaller)
4045

4146

4247
def NewJSONHTTPCloudEventConverter() -> JSONHTTPCloudEventConverter:

cloudevents/sdk/event/base.py

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

1515
import io
16-
import ujson
16+
import json
1717
import typing
1818

1919

@@ -117,43 +117,47 @@ def Set(self, key: str, value: object):
117117
def MarshalJSON(self, data_marshaller: typing.Callable) -> typing.IO:
118118
props = self.Properties()
119119
props["data"] = data_marshaller(props.get("data"))
120-
return io.StringIO(ujson.dumps(props))
120+
return io.BytesIO(json.dumps(props).encode("utf-8"))
121121

122122
def UnmarshalJSON(self, b: typing.IO,
123123
data_unmarshaller: typing.Callable):
124-
raw_ce = ujson.load(b)
124+
raw_ce = json.load(b)
125125
for name, value in raw_ce.items():
126126
if name == "data":
127127
value = data_unmarshaller(value)
128128
self.Set(name, value)
129129

130130
def UnmarshalBinary(self, headers: dict, body: typing.IO,
131131
data_unmarshaller: typing.Callable):
132-
props = self.Properties(with_nullable=True)
133-
exts = props.get("extensions")
134-
for key in props:
135-
formatted_key = "ce-{0}".format(key)
136-
if key != "extensions":
137-
self.Set(key, headers.get("ce-{0}".format(key)))
138-
if formatted_key in headers:
139-
del headers[formatted_key]
140-
141-
# rest of headers suppose to an extension?
142-
exts.update(**headers)
143-
self.Set("extensions", exts)
132+
BINARY_MAPPING = {
133+
'content-type': 'contenttype',
134+
# TODO(someone): add Distributed Tracing. It's not clear
135+
# if this is one extension or two.
136+
# https://github.com/cloudevents/spec/blob/master/extensions/distributed-tracing.md
137+
}
138+
for header, value in headers.items():
139+
header = header.lower()
140+
if header in BINARY_MAPPING:
141+
self.Set(BINARY_MAPPING[header], value)
142+
elif header.startswith("ce-"):
143+
self.Set(header[3:], value)
144+
144145
self.Set("data", data_unmarshaller(body))
145146

146-
def MarshalBinary(self) -> (dict, object):
147+
def MarshalBinary(
148+
self, data_marshaller: typing.Callable) -> (dict, object):
147149
headers = {}
150+
if self.ContentType():
151+
headers["content-type"] = self.ContentType()
148152
props = self.Properties()
149153
for key, value in props.items():
150-
if key not in ["data", "extensions"]:
154+
if key not in ["data", "extensions", "contenttype"]:
151155
if value is not None:
152156
headers["ce-{0}".format(key)] = value
153157

154-
exts = props.get("extensions")
155-
if len(exts) > 0:
156-
headers.update(**exts)
158+
for key, value in props.get("extensions"):
159+
headers["ce-{0}".format(key)] = value
157160

158161
data, _ = self.Get("data")
159-
return headers, data
162+
return headers, io.BytesIO(
163+
str(data_marshaller(data)).encode("utf-8"))

cloudevents/sdk/exceptions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ def __init__(self, event_class):
2020
"'{0}'".format(event_class))
2121

2222

23+
class InvalidDataUnmarshaller(Exception):
24+
25+
def __init__(self):
26+
super().__init__(
27+
"Invalid data unmarshaller, is not a callable")
28+
29+
2330
class InvalidDataMarshaller(Exception):
2431

2532
def __init__(self):
@@ -31,3 +38,10 @@ class NoSuchConverter(Exception):
3138
def __init__(self, converter_type):
3239
super().__init__(
3340
"No such converter {0}".format(converter_type))
41+
42+
43+
class UnsupportedEventConverter(Exception):
44+
def __init__(self, content_type):
45+
super().__init__(
46+
"Unable to identify valid event converter "
47+
"for content-type: '{0}'".format(content_type))

cloudevents/sdk/marshaller.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ def __init__(self, converters: typing.List[base.Converter]):
3535
:param converters: a list of HTTP-to-CloudEvent-to-HTTP constructors
3636
:type converters: typing.List[base.Converter]
3737
"""
38-
self.__converters = {c.TYPE: c for c in converters}
38+
self.__converters = (c for c in converters)
39+
self.__converters_by_type = {c.TYPE: c for c in converters}
3940

4041
def FromRequest(self, event: event_base.BaseEvent,
4142
headers: dict,
@@ -55,8 +56,19 @@ def FromRequest(self, event: event_base.BaseEvent,
5556
:return: a CloudEvent
5657
:rtype: event_base.BaseEvent
5758
"""
58-
for _, cnvrtr in self.__converters.items():
59-
return cnvrtr.read(event, headers, body, data_unmarshaller)
59+
if not isinstance(data_unmarshaller, typing.Callable):
60+
raise exceptions.InvalidDataUnmarshaller()
61+
62+
content_type = headers.get(
63+
"content-type", headers.get("Content-Type"))
64+
65+
for cnvrtr in self.__converters:
66+
if cnvrtr.can_read(content_type) and cnvrtr.event_supported(event):
67+
return cnvrtr.read(event, headers, body, data_unmarshaller)
68+
69+
raise exceptions.UnsupportedEventConverter(
70+
"No registered marshaller for {0} in {1}".format(
71+
content_type, self.__converters))
6072

6173
def ToRequest(self, event: event_base.BaseEvent,
6274
converter_type: str,
@@ -72,8 +84,11 @@ def ToRequest(self, event: event_base.BaseEvent,
7284
:return: dict of HTTP headers and stream of HTTP request body
7385
:rtype: tuple
7486
"""
75-
if converter_type in self.__converters:
76-
cnvrtr = self.__converters.get(converter_type)
87+
if not isinstance(data_marshaller, typing.Callable):
88+
raise exceptions.InvalidDataMarshaller()
89+
90+
if converter_type in self.__converters_by_type:
91+
cnvrtr = self.__converters_by_type[converter_type]
7792
return cnvrtr.write(event, data_marshaller)
7893

7994
raise exceptions.NoSuchConverter(converter_type)

0 commit comments

Comments
 (0)