Make SDK compliant with CloudEvents SDK spec

Signed-off-by: Denis Makogon <lildee1991@gmail.com>
This commit is contained in:
Denis Makogon 2018-12-08 09:07:41 -05:00
parent a0acdcf4af
commit 22b8b89676
14 changed files with 101 additions and 130 deletions

1
.gitignore vendored
View File

@ -123,3 +123,4 @@ venv
ChangeLog
AUTHORS
.pytest_cache/
.idea

View File

@ -6,11 +6,11 @@ Package **cloudevents** provides primitives to work with CloudEvents specificati
Parsing upstream Event from HTTP Request:
```python
from cloudevents.sdk.event import upstream
from cloudevents.sdk.event import v02
from cloudevents.sdk import marshaller
data = "<this is where your CloudEvent comes from>"
m = marshaller.NewDefaultHTTPMarshaller(upstream.Event)
m = marshaller.NewDefaultHTTPMarshaller(v02.Event)
event = m.FromRequest(
{"Content-Type": "application/cloudevents+json"},
data,
@ -25,12 +25,12 @@ from cloudevents.sdk.event import v01
event = (
v01.Event().
WithContentType("application/json").
WithData('{"name":"john"}').
WithEventID("my-id").
WithSource("from-galaxy-far-far-away").
WithEventTime("tomorrow").
WithEventType("cloudevent.greet.you")
SetContentType("application/json").
SetData('{"name":"john"}').
SetEventID("my-id").
SetSource("from-galaxy-far-far-away").
SetEventTime("tomorrow").
SetEventType("cloudevent.greet.you")
)
```
@ -44,12 +44,12 @@ from cloudevents.sdk.event import v01
event = (
v01.Event().
WithContentType("application/json").
WithData('{"name":"john"}').
WithEventID("my-id").
WithSource("from-galaxy-far-far-away").
WithEventTime("tomorrow").
WithEventType("cloudevent.greet.you")
SetContentType("application/json").
SetData('{"name":"john"}').
SetEventID("my-id").
SetSource("from-galaxy-far-far-away").
SetEventTime("tomorrow").
SetEventType("cloudevent.greet.you")
)
m = marshaller.NewHTTPMarshaller(
[

View File

@ -21,16 +21,7 @@ class Converter(object):
TYPE = None
def __init__(
self, event_class: base.BaseEvent,
supported_media_types: typing.Mapping[str, bool]):
self.event = event_class()
self.supported_media_types = supported_media_types
def can_read(self, media_type: str) -> bool:
return media_type in self.supported_media_types
def read(self, headers: dict, body: typing.IO,
def read(self, event, headers: dict, body: typing.IO,
data_unmarshaller: typing.Callable) -> base.BaseEvent:
raise Exception("not implemented")

View File

@ -17,25 +17,20 @@ import typing
from cloudevents.sdk import exceptions
from cloudevents.sdk.converters import base
from cloudevents.sdk.event import base as event_base
from cloudevents.sdk.event import v01
from cloudevents.sdk.event import v02
class BinaryHTTPCloudEventConverter(base.Converter):
TYPE = "binary"
def __init__(self, event_class: event_base.BaseEvent,
supported_media_types: typing.Mapping[str, bool]):
if event_class == v01.Event:
raise exceptions.UnsupportedEvent(event_class)
super().__init__(event_class, supported_media_types)
SUPPORTED_VERSIONS = [v02.Event, ]
def read(self,
event: event_base.BaseEvent,
headers: dict, body: typing.IO,
data_unmarshaller: typing.Callable) -> event_base.BaseEvent:
# we ignore headers, since the whole CE is in request body
event = self.event
if type(event) not in self.SUPPORTED_VERSIONS:
raise exceptions.UnsupportedEvent(type(event))
event.UnmarshalBinary(headers, body, data_unmarshaller)
return event
@ -48,11 +43,5 @@ class BinaryHTTPCloudEventConverter(base.Converter):
return hs, data_marshaller(data)
def NewBinaryHTTPCloudEventConverter(
event_class: event_base.BaseEvent) -> BinaryHTTPCloudEventConverter:
media_types = {
"application/json": True,
"application/xml": True,
"application/octet-stream": True,
}
return BinaryHTTPCloudEventConverter(event_class, media_types)
def NewBinaryHTTPCloudEventConverter() -> BinaryHTTPCloudEventConverter:
return BinaryHTTPCloudEventConverter()

View File

@ -23,15 +23,10 @@ class JSONHTTPCloudEventConverter(base.Converter):
TYPE = "structured"
def __init__(self, event_class: event_base.BaseEvent,
supported_media_types: typing.Mapping[str, bool]):
super().__init__(event_class, supported_media_types)
def read(self, headers: dict,
def read(self, event: event_base.BaseEvent,
headers: dict,
body: typing.IO,
data_unmarshaller: typing.Callable) -> event_base.BaseEvent:
# we ignore headers, since the whole CE is in request body
event = self.event
event.UnmarshalJSON(body, data_unmarshaller)
return event
@ -44,10 +39,5 @@ class JSONHTTPCloudEventConverter(base.Converter):
return {}, event.MarshalJSON(data_marshaller)
def NewJSONHTTPCloudEventConverter(
event_class: event_base.BaseEvent) -> JSONHTTPCloudEventConverter:
media_types = {
"application/cloudevents+json": True,
}
return JSONHTTPCloudEventConverter(event_class, media_types)
def NewJSONHTTPCloudEventConverter() -> JSONHTTPCloudEventConverter:
return JSONHTTPCloudEventConverter()

View File

@ -50,28 +50,28 @@ class EventGetterSetter(object):
# CloudEvent attribute constructors
# Each setter return an instance of its class
# in order to build a pipeline of setter
def WithEventType(self, eventType: str) -> object:
def SetEventType(self, eventType: str) -> object:
raise Exception("not implemented")
def WithSource(self, source: str) -> object:
def SetSource(self, source: str) -> object:
raise Exception("not implemented")
def WithEventID(self, eventID: str) -> object:
def SetEventID(self, eventID: str) -> object:
raise Exception("not implemented")
def WithEventTime(self, eventTime: str) -> object:
def SetEventTime(self, eventTime: str) -> object:
raise Exception("not implemented")
def WithSchemaURL(self, schemaURL: str) -> object:
def SetSchemaURL(self, schemaURL: str) -> object:
raise Exception("not implemented")
def WithData(self, data: object) -> object:
def SetData(self, data: object) -> object:
raise Exception("not implemented")
def WithExtensions(self, extensions: dict) -> object:
def SetExtensions(self, extensions: dict) -> object:
raise Exception("not implemented")
def WithContentType(self, contentType: str) -> object:
def SetContentType(self, contentType: str) -> object:
raise Exception("not implemented")

View File

@ -59,35 +59,35 @@ class Event(base.BaseEvent):
def ContentType(self) -> str:
return self.ce__contentType.get()
def WithEventType(self, eventType: str) -> base.BaseEvent:
def SetEventType(self, eventType: str) -> base.BaseEvent:
self.Set("eventType", eventType)
return self
def WithSource(self, source: str) -> base.BaseEvent:
def SetSource(self, source: str) -> base.BaseEvent:
self.Set("source", source)
return self
def WithEventID(self, eventID: str) -> base.BaseEvent:
def SetEventID(self, eventID: str) -> base.BaseEvent:
self.Set("eventID", eventID)
return self
def WithEventTime(self, eventTime: str) -> base.BaseEvent:
def SetEventTime(self, eventTime: str) -> base.BaseEvent:
self.Set("eventTime", eventTime)
return self
def WithSchemaURL(self, schemaURL: str) -> base.BaseEvent:
def SetSchemaURL(self, schemaURL: str) -> base.BaseEvent:
self.Set("schemaURL", schemaURL)
return self
def WithData(self, data: object) -> base.BaseEvent:
def SetData(self, data: object) -> base.BaseEvent:
self.Set("data", data)
return self
def WithExtensions(self, extensions: dict) -> base.BaseEvent:
def SetExtensions(self, extensions: dict) -> base.BaseEvent:
self.Set("extension", extensions)
return self
def WithContentType(self, contentType: str) -> base.BaseEvent:
def SetContentType(self, contentType: str) -> base.BaseEvent:
self.Set("contentType", contentType)
return self

View File

@ -19,7 +19,7 @@ from cloudevents.sdk.event import base
class Event(base.BaseEvent):
def __init__(self):
self.ce__specversion = opt.Option("specversion", "0.1", True)
self.ce__specversion = opt.Option("specversion", "0.2", True)
self.ce__type = opt.Option("type", None, True)
self.ce__source = opt.Option("source", None, True)
self.ce__id = opt.Option("id", None, True)
@ -56,34 +56,34 @@ class Event(base.BaseEvent):
def ContentType(self) -> str:
return self.ce__contenttype.get()
def WithEventType(self, eventType: str) -> base.BaseEvent:
def SetEventType(self, eventType: str) -> base.BaseEvent:
self.Set("type", eventType)
return self
def WithSource(self, source: str) -> base.BaseEvent:
def SetSource(self, source: str) -> base.BaseEvent:
self.Set("source", source)
return self
def WithEventID(self, eventID: str) -> base.BaseEvent:
def SetEventID(self, eventID: str) -> base.BaseEvent:
self.Set("id", eventID)
return self
def WithEventTime(self, eventTime: str) -> base.BaseEvent:
def SetEventTime(self, eventTime: str) -> base.BaseEvent:
self.Set("time", eventTime)
return self
def WithSchemaURL(self, schemaURL: str) -> base.BaseEvent:
def SetSchemaURL(self, schemaURL: str) -> base.BaseEvent:
self.Set("schemaurl", schemaURL)
return self
def WithData(self, data: object) -> base.BaseEvent:
def SetData(self, data: object) -> base.BaseEvent:
self.Set("data", data)
return self
def WithExtensions(self, extensions: dict) -> base.BaseEvent:
def SetExtensions(self, extensions: dict) -> base.BaseEvent:
self.Set("extension", extensions)
return self
def WithContentType(self, contentType: str) -> base.BaseEvent:
def SetContentType(self, contentType: str) -> base.BaseEvent:
self.Set("contenttype", contentType)
return self

View File

@ -13,14 +13,6 @@
# under the License.
class InvalidMimeTypeFromRequest(Exception):
def __init__(self, mime_type):
super().__init__(
"Unable to read CloudEvent from request, "
"invalid MIME type: {0}".format(mime_type))
class UnsupportedEvent(Exception):
def __init__(self, event_class):

View File

@ -37,12 +37,15 @@ class HTTPMarshaller(object):
"""
self.__converters = {c.TYPE: c for c in converters}
def FromRequest(self, headers: dict,
def FromRequest(self, event: event_base.BaseEvent,
headers: dict,
body: typing.IO,
data_unmarshaller:
typing.Callable) -> event_base.BaseEvent:
"""
Reads a CloudEvent from an HTTP headers and request body
:param event: CloudEvent placeholder
:type event: cloudevents.sdk.event.base.BaseEvent
:param headers: a dict-like HTTP headers
:type headers: dict
:param body: a stream-like HTTP request body
@ -52,12 +55,8 @@ class HTTPMarshaller(object):
:return: a CloudEvent
:rtype: event_base.BaseEvent
"""
mimeType = headers.get("Content-Type")
for _, cnvrtr in self.__converters.items():
if cnvrtr.can_read(mimeType):
return cnvrtr.read(headers, body, data_unmarshaller)
raise exceptions.InvalidMimeTypeFromRequest(mimeType)
return cnvrtr.read(event, headers, body, data_unmarshaller)
def ToRequest(self, event: event_base.BaseEvent,
converter_type: str,
@ -80,19 +79,16 @@ class HTTPMarshaller(object):
raise exceptions.NoSuchConverter(converter_type)
def NewDefaultHTTPMarshaller(
event_class: event_base.BaseEvent) -> HTTPMarshaller:
def NewDefaultHTTPMarshaller() -> HTTPMarshaller:
"""
Creates the default HTTP marshaller with both structured
and binary converters
:param event_class: CloudEvent spec class
:type event_class: event_base.BaseEvent
:return: an instance of HTTP marshaller
:rtype: cloudevents.sdk.marshaller.HTTPMarshaller
"""
return HTTPMarshaller([
structured.NewJSONHTTPCloudEventConverter(event_class),
binary.NewBinaryHTTPCloudEventConverter(event_class),
structured.NewJSONHTTPCloudEventConverter(),
binary.NewBinaryHTTPCloudEventConverter(),
])

View File

@ -16,7 +16,7 @@ contentType = "application/json"
ce_type = "word.found.exclamation"
ce_id = "16fb5f0b-211e-1102-3dfe-ea6e2806f124"
source = "pytest"
specversion = "0.1"
specversion = "0.2"
eventTime = "2018-10-23T12:28:23.3464579Z"
body = '{"name":"john"}'
headers = {

View File

@ -20,7 +20,7 @@ from cloudevents.sdk import exceptions
from cloudevents.sdk import marshaller
from cloudevents.sdk.event import v01
from cloudevents.sdk.event import upstream
from cloudevents.sdk.event import v02
from cloudevents.sdk.converters import binary
from cloudevents.sdk.converters import structured
@ -31,10 +31,10 @@ from cloudevents.tests import data
def test_binary_converter_upstream():
m = marshaller.NewHTTPMarshaller(
[
binary.NewBinaryHTTPCloudEventConverter(upstream.Event)
binary.NewBinaryHTTPCloudEventConverter()
]
)
event = m.FromRequest(data.headers, None, lambda x: x)
event = m.FromRequest(v02.Event(), data.headers, None, lambda x: x)
assert event is not None
assert event.Get("type") == (data.ce_type, True)
assert event.Get("id") == (data.ce_id, True)
@ -43,10 +43,11 @@ def test_binary_converter_upstream():
def test_structured_converter_upstream():
m = marshaller.NewHTTPMarshaller(
[
structured.NewJSONHTTPCloudEventConverter(upstream.Event)
structured.NewJSONHTTPCloudEventConverter()
]
)
event = m.FromRequest(
v02.Event(),
{"Content-Type": "application/cloudevents+json"},
io.StringIO(ujson.dumps(data.ce)),
lambda x: x.read()
@ -59,19 +60,26 @@ def test_structured_converter_upstream():
# todo: clarify whether spec 0.1 doesn't support binary format
def test_binary_converter_v01():
m = marshaller.NewHTTPMarshaller(
[
binary.NewBinaryHTTPCloudEventConverter()
]
)
pytest.raises(
exceptions.UnsupportedEvent,
binary.NewBinaryHTTPCloudEventConverter,
v01.Event)
m.FromRequest,
v01.Event, None, None, None)
def test_structured_converter_v01():
m = marshaller.NewHTTPMarshaller(
[
structured.NewJSONHTTPCloudEventConverter(v01.Event)
structured.NewJSONHTTPCloudEventConverter()
]
)
event = m.FromRequest(
v01.Event(),
{"Content-Type": "application/cloudevents+json"},
io.StringIO(ujson.dumps(data.ce)),
lambda x: x.read()
@ -83,9 +91,10 @@ def test_structured_converter_v01():
def test_default_http_marshaller():
m = marshaller.NewDefaultHTTPMarshaller(upstream.Event)
m = marshaller.NewDefaultHTTPMarshaller()
event = m.FromRequest(
v02.Event(),
{"Content-Type": "application/cloudevents+json"},
io.StringIO(ujson.dumps(data.ce)),
lambda x: x.read()

View File

@ -15,7 +15,7 @@
import ujson
from cloudevents.sdk.event import v01
from cloudevents.sdk.event import upstream
from cloudevents.sdk.event import v02
from cloudevents.sdk import converters
from cloudevents.sdk import marshaller
@ -26,15 +26,15 @@ from cloudevents.tests import data
def test_event_pipeline_upstream():
event = (
upstream.Event().
WithContentType(data.contentType).
WithData(data.body).
WithEventID(data.ce_id).
WithSource(data.source).
WithEventTime(data.eventTime).
WithEventType(data.ce_type)
v02.Event().
SetContentType(data.contentType).
SetData(data.body).
SetEventID(data.ce_id).
SetSource(data.source).
SetEventTime(data.eventTime).
SetEventType(data.ce_type)
)
m = marshaller.NewDefaultHTTPMarshaller(type(event))
m = marshaller.NewDefaultHTTPMarshaller()
new_headers, body = m.ToRequest(event, converters.TypeBinary, lambda x: x)
assert new_headers is not None
assert "ce-specversion" in new_headers
@ -49,16 +49,16 @@ def test_event_pipeline_upstream():
def test_event_pipeline_v01():
event = (
v01.Event().
WithContentType(data.contentType).
WithData(data.body).
WithEventID(data.ce_id).
WithSource(data.source).
WithEventTime(data.eventTime).
WithEventType(data.ce_type)
SetContentType(data.contentType).
SetData(data.body).
SetEventID(data.ce_id).
SetSource(data.source).
SetEventTime(data.eventTime).
SetEventType(data.ce_type)
)
m = marshaller.NewHTTPMarshaller(
[
structured.NewJSONHTTPCloudEventConverter(type(event))
structured.NewJSONHTTPCloudEventConverter()
]
)

View File

@ -21,15 +21,16 @@ from cloudevents.sdk import marshaller
from cloudevents.sdk.converters import structured
from cloudevents.sdk.event import v01
from cloudevents.sdk.event import upstream
from cloudevents.sdk.event import v02
from cloudevents.tests import data
def test_binary_event_to_request_upstream():
m = marshaller.NewDefaultHTTPMarshaller(upstream.Event)
m = marshaller.NewDefaultHTTPMarshaller()
event = m.FromRequest(
v02.Event(),
{"Content-Type": "application/cloudevents+json"},
io.StringIO(ujson.dumps(data.ce)),
lambda x: x.read()
@ -46,8 +47,9 @@ def test_binary_event_to_request_upstream():
def test_structured_event_to_request_upstream():
copy_of_ce = copy.deepcopy(data.ce)
m = marshaller.NewDefaultHTTPMarshaller(upstream.Event)
m = marshaller.NewDefaultHTTPMarshaller()
event = m.FromRequest(
v02.Event(),
{"Content-Type": "application/cloudevents+json"},
io.StringIO(ujson.dumps(data.ce)),
lambda x: x.read()
@ -65,10 +67,11 @@ def test_structured_event_to_request_v01():
copy_of_ce = copy.deepcopy(data.ce)
m = marshaller.NewHTTPMarshaller(
[
structured.NewJSONHTTPCloudEventConverter(v01.Event)
structured.NewJSONHTTPCloudEventConverter()
]
)
event = m.FromRequest(
v01.Event(),
{"Content-Type": "application/cloudevents+json"},
io.StringIO(ujson.dumps(data.ce)),
lambda x: x.read()