diff --git a/.gitignore b/.gitignore index 9c8a0d8..420bc8f 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,4 @@ venv ChangeLog AUTHORS .pytest_cache/ +.idea diff --git a/README.md b/README.md index 80f02b6..dcc11a8 100644 --- a/README.md +++ b/README.md @@ -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 = "" -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( [ diff --git a/cloudevents/sdk/converters/base.py b/cloudevents/sdk/converters/base.py index 97545f0..2d9755c 100644 --- a/cloudevents/sdk/converters/base.py +++ b/cloudevents/sdk/converters/base.py @@ -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") diff --git a/cloudevents/sdk/converters/binary.py b/cloudevents/sdk/converters/binary.py index 7b18acd..8c03522 100644 --- a/cloudevents/sdk/converters/binary.py +++ b/cloudevents/sdk/converters/binary.py @@ -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() diff --git a/cloudevents/sdk/converters/structured.py b/cloudevents/sdk/converters/structured.py index be2bdd0..e885b2c 100644 --- a/cloudevents/sdk/converters/structured.py +++ b/cloudevents/sdk/converters/structured.py @@ -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() diff --git a/cloudevents/sdk/event/base.py b/cloudevents/sdk/event/base.py index b27be3a..6868365 100644 --- a/cloudevents/sdk/event/base.py +++ b/cloudevents/sdk/event/base.py @@ -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") diff --git a/cloudevents/sdk/event/v01.py b/cloudevents/sdk/event/v01.py index 9c2fc30..bc05670 100644 --- a/cloudevents/sdk/event/v01.py +++ b/cloudevents/sdk/event/v01.py @@ -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 diff --git a/cloudevents/sdk/event/upstream.py b/cloudevents/sdk/event/v02.py similarity index 80% rename from cloudevents/sdk/event/upstream.py rename to cloudevents/sdk/event/v02.py index 551b73e..8da19c7 100644 --- a/cloudevents/sdk/event/upstream.py +++ b/cloudevents/sdk/event/v02.py @@ -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 diff --git a/cloudevents/sdk/exceptions.py b/cloudevents/sdk/exceptions.py index ec66904..b546dfe 100644 --- a/cloudevents/sdk/exceptions.py +++ b/cloudevents/sdk/exceptions.py @@ -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): diff --git a/cloudevents/sdk/marshaller.py b/cloudevents/sdk/marshaller.py index 7460dba..4ad4680 100644 --- a/cloudevents/sdk/marshaller.py +++ b/cloudevents/sdk/marshaller.py @@ -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(), ]) diff --git a/cloudevents/tests/data.py b/cloudevents/tests/data.py index 2846ca3..47c2a8d 100644 --- a/cloudevents/tests/data.py +++ b/cloudevents/tests/data.py @@ -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 = { diff --git a/cloudevents/tests/test_event_from_request_converter.py b/cloudevents/tests/test_event_from_request_converter.py index 0d7944f..14a962e 100644 --- a/cloudevents/tests/test_event_from_request_converter.py +++ b/cloudevents/tests/test_event_from_request_converter.py @@ -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() diff --git a/cloudevents/tests/test_event_pipeline.py b/cloudevents/tests/test_event_pipeline.py index b4f7462..9ce793f 100644 --- a/cloudevents/tests/test_event_pipeline.py +++ b/cloudevents/tests/test_event_pipeline.py @@ -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() ] ) diff --git a/cloudevents/tests/test_event_to_request_converter.py b/cloudevents/tests/test_event_to_request_converter.py index 80154b7..ac3f4c3 100644 --- a/cloudevents/tests/test_event_to_request_converter.py +++ b/cloudevents/tests/test_event_to_request_converter.py @@ -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()