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 ChangeLog
AUTHORS AUTHORS
.pytest_cache/ .pytest_cache/
.idea

View File

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

View File

@ -21,16 +21,7 @@ class Converter(object):
TYPE = None TYPE = None
def __init__( def read(self, event, headers: dict, body: typing.IO,
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,
data_unmarshaller: typing.Callable) -> base.BaseEvent: data_unmarshaller: typing.Callable) -> base.BaseEvent:
raise Exception("not implemented") raise Exception("not implemented")

View File

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

View File

@ -23,15 +23,10 @@ class JSONHTTPCloudEventConverter(base.Converter):
TYPE = "structured" TYPE = "structured"
def __init__(self, event_class: event_base.BaseEvent, def read(self, event: event_base.BaseEvent,
supported_media_types: typing.Mapping[str, bool]): headers: dict,
super().__init__(event_class, supported_media_types)
def read(self, headers: dict,
body: typing.IO, body: typing.IO,
data_unmarshaller: typing.Callable) -> event_base.BaseEvent: 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) event.UnmarshalJSON(body, data_unmarshaller)
return event return event
@ -44,10 +39,5 @@ class JSONHTTPCloudEventConverter(base.Converter):
return {}, event.MarshalJSON(data_marshaller) return {}, event.MarshalJSON(data_marshaller)
def NewJSONHTTPCloudEventConverter( def NewJSONHTTPCloudEventConverter() -> JSONHTTPCloudEventConverter:
event_class: event_base.BaseEvent) -> JSONHTTPCloudEventConverter: return JSONHTTPCloudEventConverter()
media_types = {
"application/cloudevents+json": True,
}
return JSONHTTPCloudEventConverter(event_class, media_types)

View File

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

View File

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

View File

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

View File

@ -13,14 +13,6 @@
# under the License. # 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): class UnsupportedEvent(Exception):
def __init__(self, event_class): def __init__(self, event_class):

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@
import ujson import ujson
from cloudevents.sdk.event import v01 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 converters
from cloudevents.sdk import marshaller from cloudevents.sdk import marshaller
@ -26,15 +26,15 @@ from cloudevents.tests import data
def test_event_pipeline_upstream(): def test_event_pipeline_upstream():
event = ( event = (
upstream.Event(). v02.Event().
WithContentType(data.contentType). SetContentType(data.contentType).
WithData(data.body). SetData(data.body).
WithEventID(data.ce_id). SetEventID(data.ce_id).
WithSource(data.source). SetSource(data.source).
WithEventTime(data.eventTime). SetEventTime(data.eventTime).
WithEventType(data.ce_type) SetEventType(data.ce_type)
) )
m = marshaller.NewDefaultHTTPMarshaller(type(event)) m = marshaller.NewDefaultHTTPMarshaller()
new_headers, body = m.ToRequest(event, converters.TypeBinary, lambda x: x) new_headers, body = m.ToRequest(event, converters.TypeBinary, lambda x: x)
assert new_headers is not None assert new_headers is not None
assert "ce-specversion" in new_headers assert "ce-specversion" in new_headers
@ -49,16 +49,16 @@ def test_event_pipeline_upstream():
def test_event_pipeline_v01(): def test_event_pipeline_v01():
event = ( event = (
v01.Event(). v01.Event().
WithContentType(data.contentType). SetContentType(data.contentType).
WithData(data.body). SetData(data.body).
WithEventID(data.ce_id). SetEventID(data.ce_id).
WithSource(data.source). SetSource(data.source).
WithEventTime(data.eventTime). SetEventTime(data.eventTime).
WithEventType(data.ce_type) SetEventType(data.ce_type)
) )
m = marshaller.NewHTTPMarshaller( 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.converters import structured
from cloudevents.sdk.event import v01 from cloudevents.sdk.event import v01
from cloudevents.sdk.event import upstream from cloudevents.sdk.event import v02
from cloudevents.tests import data from cloudevents.tests import data
def test_binary_event_to_request_upstream(): def test_binary_event_to_request_upstream():
m = marshaller.NewDefaultHTTPMarshaller(upstream.Event) m = marshaller.NewDefaultHTTPMarshaller()
event = m.FromRequest( event = m.FromRequest(
v02.Event(),
{"Content-Type": "application/cloudevents+json"}, {"Content-Type": "application/cloudevents+json"},
io.StringIO(ujson.dumps(data.ce)), io.StringIO(ujson.dumps(data.ce)),
lambda x: x.read() lambda x: x.read()
@ -46,8 +47,9 @@ def test_binary_event_to_request_upstream():
def test_structured_event_to_request_upstream(): def test_structured_event_to_request_upstream():
copy_of_ce = copy.deepcopy(data.ce) copy_of_ce = copy.deepcopy(data.ce)
m = marshaller.NewDefaultHTTPMarshaller(upstream.Event) m = marshaller.NewDefaultHTTPMarshaller()
event = m.FromRequest( event = m.FromRequest(
v02.Event(),
{"Content-Type": "application/cloudevents+json"}, {"Content-Type": "application/cloudevents+json"},
io.StringIO(ujson.dumps(data.ce)), io.StringIO(ujson.dumps(data.ce)),
lambda x: x.read() lambda x: x.read()
@ -65,10 +67,11 @@ def test_structured_event_to_request_v01():
copy_of_ce = copy.deepcopy(data.ce) copy_of_ce = copy.deepcopy(data.ce)
m = marshaller.NewHTTPMarshaller( m = marshaller.NewHTTPMarshaller(
[ [
structured.NewJSONHTTPCloudEventConverter(v01.Event) structured.NewJSONHTTPCloudEventConverter()
] ]
) )
event = m.FromRequest( event = m.FromRequest(
v01.Event(),
{"Content-Type": "application/cloudevents+json"}, {"Content-Type": "application/cloudevents+json"},
io.StringIO(ujson.dumps(data.ce)), io.StringIO(ujson.dumps(data.ce)),
lambda x: x.read() lambda x: x.read()