diff --git a/CHANGELOG.md b/CHANGELOG.md index 84916b7..ef5c756 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Added Cloudevents V0.3 and V1 implementations ([#22]) - Add helpful text to README ([#23]) - Add link to email in README ([#27]) @@ -61,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#15]: https://github.com/cloudevents/sdk-python/pull/15 [#17]: https://github.com/cloudevents/sdk-python/pull/17 [#21]: https://github.com/cloudevents/sdk-python/pull/21 +[#22]: https://github.com/cloudevents/sdk-python/pull/22 [#23]: https://github.com/cloudevents/sdk-python/pull/23 [#25]: https://github.com/cloudevents/sdk-python/pull/25 [#27]: https://github.com/cloudevents/sdk-python/pull/27 diff --git a/cloudevents/sdk/converters/binary.py b/cloudevents/sdk/converters/binary.py index 84f4d4c..97c4e44 100644 --- a/cloudevents/sdk/converters/binary.py +++ b/cloudevents/sdk/converters/binary.py @@ -17,13 +17,13 @@ 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 v02 +from cloudevents.sdk.event import v02, v03, v1 class BinaryHTTPCloudEventConverter(base.Converter): TYPE = "binary" - SUPPORTED_VERSIONS = [v02.Event] + SUPPORTED_VERSIONS = [v02.Event, v03.Event, v1.Event] def can_read(self, content_type: str) -> bool: return True diff --git a/cloudevents/sdk/event/base.py b/cloudevents/sdk/event/base.py index c21ee60..d392ae8 100644 --- a/cloudevents/sdk/event/base.py +++ b/cloudevents/sdk/event/base.py @@ -17,7 +17,9 @@ import json import typing +# TODO(slinkydeveloper) is this really needed? class EventGetterSetter(object): + def CloudEventVersion(self) -> str: raise Exception("not implemented") @@ -126,16 +128,10 @@ class BaseEvent(EventGetterSetter): body: typing.IO, data_unmarshaller: typing.Callable ): - binary_mapping = { - "content-type": "contenttype", - # TODO(someone): add Distributed Tracing. It's not clear - # if this is one extension or two. - # https://github.com/cloudevents/spec/blob/master/extensions/distributed-tracing.md - } for header, value in headers.items(): header = header.lower() - if header in binary_mapping: - self.Set(binary_mapping[header], value) + if header == "content-type": + self.SetContentType(value) elif header.startswith("ce-"): self.Set(header[3:], value) diff --git a/cloudevents/sdk/event/v03.py b/cloudevents/sdk/event/v03.py new file mode 100644 index 0000000..4207e40 --- /dev/null +++ b/cloudevents/sdk/event/v03.py @@ -0,0 +1,109 @@ +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cloudevents.sdk.event import base +from cloudevents.sdk.event import opt + + +class Event(base.BaseEvent): + def __init__(self): + self.ce__specversion = opt.Option("specversion", "0.3", True) + self.ce__id = opt.Option("id", None, True) + self.ce__source = opt.Option("source", None, True) + self.ce__type = opt.Option("type", None, True) + + self.ce__datacontenttype = opt.Option("datacontenttype", None, False) + self.ce__datacontentencoding = opt.Option( + "datacontentencoding", + None, + False + ) + self.ce__subject = opt.Option("subject", None, False) + self.ce__time = opt.Option("time", None, False) + self.ce__schemaurl = opt.Option("schemaurl", None, False) + self.ce__data = opt.Option("data", None, False) + self.ce__extensions = opt.Option("extensions", dict(), False) + + def CloudEventVersion(self) -> str: + return self.ce__specversion.get() + + def EventType(self) -> str: + return self.ce__type.get() + + def Source(self) -> str: + return self.ce__source.get() + + def EventID(self) -> str: + return self.ce__id.get() + + def EventTime(self) -> str: + return self.ce__time.get() + + def Subject(self) -> str: + return self.ce__subject.get() + + def SchemaURL(self) -> str: + return self.ce__schemaurl.get() + + def Data(self) -> object: + return self.ce__data.get() + + def Extensions(self) -> dict: + return self.ce__extensions.get() + + def ContentType(self) -> str: + return self.ce__datacontenttype.get() + + def ContentEncoding(self) -> str: + return self.ce__datacontentencoding.get() + + def SetEventType(self, eventType: str) -> base.BaseEvent: + self.Set("type", eventType) + return self + + def SetSource(self, source: str) -> base.BaseEvent: + self.Set("source", source) + return self + + def SetEventID(self, eventID: str) -> base.BaseEvent: + self.Set("id", eventID) + return self + + def SetEventTime(self, eventTime: str) -> base.BaseEvent: + self.Set("time", eventTime) + return self + + def SetSubject(self, subject: str) -> base.BaseEvent: + self.Set("subject", subject) + return self + + def SetSchemaURL(self, schemaURL: str) -> base.BaseEvent: + self.Set("schemaurl", schemaURL) + return self + + def SetData(self, data: object) -> base.BaseEvent: + self.Set("data", data) + return self + + def SetExtensions(self, extensions: dict) -> base.BaseEvent: + self.Set("extensions", extensions) + return self + + def SetContentType(self, contentType: str) -> base.BaseEvent: + self.Set("datacontenttype", contentType) + return self + + def SetContentEncoding(self, contentEncoding: str) -> base.BaseEvent: + self.Set("datacontentencoding", contentEncoding) + return self diff --git a/cloudevents/sdk/event/v1.py b/cloudevents/sdk/event/v1.py new file mode 100644 index 0000000..655111a --- /dev/null +++ b/cloudevents/sdk/event/v1.py @@ -0,0 +1,97 @@ +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cloudevents.sdk.event import base +from cloudevents.sdk.event import opt + + +class Event(base.BaseEvent): + def __init__(self): + self.ce__specversion = opt.Option("specversion", "1.0", True) + self.ce__id = opt.Option("id", None, True) + self.ce__source = opt.Option("source", None, True) + self.ce__type = opt.Option("type", None, True) + + self.ce__datacontenttype = opt.Option("datacontenttype", None, False) + self.ce__dataschema = opt.Option("dataschema", None, False) + self.ce__subject = opt.Option("subject", None, False) + self.ce__time = opt.Option("time", None, False) + self.ce__data = opt.Option("data", None, False) + self.ce__extensions = opt.Option("extensions", dict(), False) + + def CloudEventVersion(self) -> str: + return self.ce__specversion.get() + + def EventType(self) -> str: + return self.ce__type.get() + + def Source(self) -> str: + return self.ce__source.get() + + def EventID(self) -> str: + return self.ce__id.get() + + def EventTime(self) -> str: + return self.ce__time.get() + + def Subject(self) -> str: + return self.ce__subject.get() + + def Schema(self) -> str: + return self.ce__dataschema.get() + + def ContentType(self) -> str: + return self.ce__datacontenttype.get() + + def Data(self) -> object: + return self.ce__data.get() + + def Extensions(self) -> dict: + return self.ce__extensions.get() + + def SetEventType(self, eventType: str) -> base.BaseEvent: + self.Set("type", eventType) + return self + + def SetSource(self, source: str) -> base.BaseEvent: + self.Set("source", source) + return self + + def SetEventID(self, eventID: str) -> base.BaseEvent: + self.Set("id", eventID) + return self + + def SetEventTime(self, eventTime: str) -> base.BaseEvent: + self.Set("time", eventTime) + return self + + def SetSubject(self, subject: str) -> base.BaseEvent: + self.Set("subject", subject) + return self + + def SetSchema(self, schema: str) -> base.BaseEvent: + self.Set("dataschema", schema) + return self + + def SetContentType(self, contentType: str) -> base.BaseEvent: + self.Set("datacontenttype", contentType) + return self + + def SetData(self, data: object) -> base.BaseEvent: + self.Set("data", data) + return self + + def SetExtensions(self, extensions: dict) -> base.BaseEvent: + self.Set("extensions", extensions) + return self diff --git a/cloudevents/sdk/marshaller.py b/cloudevents/sdk/marshaller.py index 22a2b70..a54a135 100644 --- a/cloudevents/sdk/marshaller.py +++ b/cloudevents/sdk/marshaller.py @@ -61,7 +61,9 @@ class HTTPMarshaller(object): if not isinstance(data_unmarshaller, typing.Callable): raise exceptions.InvalidDataUnmarshaller() - content_type = headers.get("content-type", headers.get("Content-Type")) + # Lower all header keys + headers = {key.lower(): value for key, value in headers.items()} + content_type = headers.get("content-type", None) for cnvrtr in self.__converters: if cnvrtr.can_read(content_type) and cnvrtr.event_supported(event): diff --git a/cloudevents/tests/data.py b/cloudevents/tests/data.py index 534bb2f..e1d615f 100644 --- a/cloudevents/tests/data.py +++ b/cloudevents/tests/data.py @@ -12,26 +12,65 @@ # License for the specific language governing permissions and limitations # under the License. +from cloudevents.sdk.event import v02, v03, v1 + contentType = "application/json" ce_type = "word.found.exclamation" ce_id = "16fb5f0b-211e-1102-3dfe-ea6e2806f124" source = "pytest" -specversion = "0.2" eventTime = "2018-10-23T12:28:23.3464579Z" body = '{"name":"john"}' + headers = { - "ce-specversion": specversion, - "ce-type": ce_type, - "ce-id": ce_id, - "ce-time": eventTime, - "ce-source": source, - "Content-Type": contentType, + v02.Event: { + "ce-specversion": "0.2", + "ce-type": ce_type, + "ce-id": ce_id, + "ce-time": eventTime, + "ce-source": source, + "Content-Type": contentType, + }, + v03.Event: { + "ce-specversion": "0.3", + "ce-type": ce_type, + "ce-id": ce_id, + "ce-time": eventTime, + "ce-source": source, + "Content-Type": contentType, + }, + v1.Event: { + "ce-specversion": "1.0", + "ce-type": ce_type, + "ce-id": ce_id, + "ce-time": eventTime, + "ce-source": source, + "Content-Type": contentType, + }, } -ce = { - "specversion": specversion, - "type": ce_type, - "id": ce_id, - "time": eventTime, - "source": source, - "contenttype": contentType, + +json_ce = { + v02.Event: { + "specversion": "0.2", + "type": ce_type, + "id": ce_id, + "time": eventTime, + "source": source, + "contenttype": contentType, + }, + v03.Event: { + "specversion": "0.3", + "type": ce_type, + "id": ce_id, + "time": eventTime, + "source": source, + "datacontenttype": contentType, + }, + v1.Event: { + "specversion": "1.0", + "type": ce_type, + "id": ce_id, + "time": eventTime, + "source": source, + "datacontenttype": contentType, + }, } diff --git a/cloudevents/tests/test_event_from_request_converter.py b/cloudevents/tests/test_event_from_request_converter.py index ea3baec..76930c5 100644 --- a/cloudevents/tests/test_event_from_request_converter.py +++ b/cloudevents/tests/test_event_from_request_converter.py @@ -21,6 +21,8 @@ from cloudevents.sdk import marshaller from cloudevents.sdk.event import v01 from cloudevents.sdk.event import v02 +from cloudevents.sdk.event import v03 +from cloudevents.sdk.event import v1 from cloudevents.sdk.converters import binary from cloudevents.sdk.converters import structured @@ -28,28 +30,32 @@ from cloudevents.sdk.converters import structured from cloudevents.tests import data -def test_binary_converter_upstream(): +@pytest.mark.parametrize("event_class", [v02.Event, v03.Event, v1.Event]) +def test_binary_converter_upstream(event_class): m = marshaller.NewHTTPMarshaller( [binary.NewBinaryHTTPCloudEventConverter()]) - event = m.FromRequest(v02.Event(), data.headers, None, lambda x: x) + event = m.FromRequest(event_class(), data.headers[event_class], 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) + assert event.EventType() == data.ce_type + assert event.EventID() == data.ce_id + assert event.ContentType() == data.contentType -def test_structured_converter_upstream(): +@pytest.mark.parametrize("event_class", [v02.Event, v03.Event, v1.Event]) +def test_structured_converter_upstream(event_class): m = marshaller.NewHTTPMarshaller( [structured.NewJSONHTTPCloudEventConverter()]) event = m.FromRequest( - v02.Event(), + event_class(), {"Content-Type": "application/cloudevents+json"}, - io.StringIO(json.dumps(data.ce)), + io.StringIO(json.dumps(data.json_ce[event_class])), lambda x: x.read(), ) assert event is not None - assert event.Get("type") == (data.ce_type, True) - assert event.Get("id") == (data.ce_id, True) + assert event.EventType() == data.ce_type + assert event.EventID() == data.ce_id + assert event.ContentType() == data.contentType def test_binary_converter_v01(): @@ -86,7 +92,7 @@ def test_structured_converter_v01(): event = m.FromRequest( v01.Event(), {"Content-Type": "application/cloudevents+json"}, - io.StringIO(json.dumps(data.ce)), + io.StringIO(json.dumps(data.json_ce[v02.Event])), lambda x: x.read(), ) @@ -94,33 +100,36 @@ def test_structured_converter_v01(): assert event.Get("type") == (data.ce_type, True) assert event.Get("id") == (data.ce_id, True) - -def test_default_http_marshaller_with_structured(): +@pytest.mark.parametrize("event_class", [v02.Event, v03.Event, v1.Event]) +def test_default_http_marshaller_with_structured(event_class): m = marshaller.NewDefaultHTTPMarshaller() event = m.FromRequest( - v02.Event(), + event_class(), {"Content-Type": "application/cloudevents+json"}, - io.StringIO(json.dumps(data.ce)), + io.StringIO(json.dumps(data.json_ce[event_class])), lambda x: x.read(), ) assert event is not None - assert event.Get("type") == (data.ce_type, True) - assert event.Get("id") == (data.ce_id, True) + assert event.EventType() == data.ce_type + assert event.EventID() == data.ce_id + assert event.ContentType() == data.contentType -def test_default_http_marshaller_with_binary(): +@pytest.mark.parametrize("event_class", [v02.Event, v03.Event, v1.Event]) +def test_default_http_marshaller_with_binary(event_class): m = marshaller.NewDefaultHTTPMarshaller() event = m.FromRequest( - v02.Event(), data.headers, + event_class(), data.headers[event_class], io.StringIO(json.dumps(data.body)), json.load ) assert event is not None - assert event.Get("type") == (data.ce_type, True) - assert event.Get("data") == (data.body, True) - assert event.Get("id") == (data.ce_id, True) + assert event.EventType() == data.ce_type + assert event.EventID() == data.ce_id + assert event.ContentType() == data.contentType + assert event.Data() == data.body def test_unsupported_event_configuration(): @@ -131,7 +140,7 @@ def test_unsupported_event_configuration(): m.FromRequest, v01.Event(), {"Content-Type": "application/cloudevents+json"}, - io.StringIO(json.dumps(data.ce)), + io.StringIO(json.dumps(data.json_ce[v02.Event])), lambda x: x.read(), ) diff --git a/cloudevents/tests/test_event_pipeline.py b/cloudevents/tests/test_event_pipeline.py index c65248f..554d8b2 100644 --- a/cloudevents/tests/test_event_pipeline.py +++ b/cloudevents/tests/test_event_pipeline.py @@ -14,9 +14,9 @@ import io import json +import pytest -from cloudevents.sdk.event import v01 -from cloudevents.sdk.event import v02 +from cloudevents.sdk.event import v01, v02, v03, v1 from cloudevents.sdk import converters from cloudevents.sdk import marshaller @@ -24,10 +24,10 @@ from cloudevents.sdk.converters import structured from cloudevents.tests import data - -def test_event_pipeline_upstream(): +@pytest.mark.parametrize("event_class", [v02.Event, v03.Event, v1.Event]) +def test_event_pipeline_upstream(event_class): event = ( - v02.Event() + event_class() .SetContentType(data.contentType) .SetData(data.body) .SetEventID(data.ce_id) diff --git a/cloudevents/tests/test_event_to_request_converter.py b/cloudevents/tests/test_event_to_request_converter.py index ebbaa6e..0719035 100644 --- a/cloudevents/tests/test_event_to_request_converter.py +++ b/cloudevents/tests/test_event_to_request_converter.py @@ -15,46 +15,51 @@ import io import json import copy +import pytest from cloudevents.sdk import converters from cloudevents.sdk import marshaller from cloudevents.sdk.converters import structured -from cloudevents.sdk.event import v01 +from cloudevents.sdk.event import v01, v02, v03, v1 from cloudevents.sdk.event import v02 from cloudevents.tests import data -def test_binary_event_to_request_upstream(): +@pytest.mark.parametrize("event_class", [v02.Event, v03.Event, v1.Event]) +def test_binary_event_to_request_upstream(event_class): m = marshaller.NewDefaultHTTPMarshaller() event = m.FromRequest( - v02.Event(), + event_class(), {"Content-Type": "application/cloudevents+json"}, - io.StringIO(json.dumps(data.ce)), + io.StringIO(json.dumps(data.json_ce[event_class])), lambda x: x.read(), ) assert event is not None - assert event.Get("type") == (data.ce_type, True) - assert event.Get("id") == (data.ce_id, True) + assert event.EventType() == data.ce_type + assert event.EventID() == data.ce_id + assert event.ContentType() == data.contentType new_headers, _ = m.ToRequest(event, converters.TypeBinary, lambda x: x) assert new_headers is not None assert "ce-specversion" in new_headers -def test_structured_event_to_request_upstream(): - copy_of_ce = copy.deepcopy(data.ce) +@pytest.mark.parametrize("event_class", [v02.Event, v03.Event, v1.Event]) +def test_structured_event_to_request_upstream(event_class): + copy_of_ce = copy.deepcopy(data.json_ce[event_class]) m = marshaller.NewDefaultHTTPMarshaller() http_headers = {"content-type": "application/cloudevents+json"} event = m.FromRequest( - v02.Event(), http_headers, io.StringIO(json.dumps(data.ce)), lambda x: x.read() + event_class(), http_headers, io.StringIO(json.dumps(data.json_ce[event_class])), lambda x: x.read() ) assert event is not None - assert event.Get("type") == (data.ce_type, True) - assert event.Get("id") == (data.ce_id, True) + assert event.EventType() == data.ce_type + assert event.EventID() == data.ce_id + assert event.ContentType() == data.contentType new_headers, _ = m.ToRequest(event, converters.TypeStructured, lambda x: x) for key in new_headers: @@ -65,11 +70,11 @@ def test_structured_event_to_request_upstream(): def test_structured_event_to_request_v01(): - copy_of_ce = copy.deepcopy(data.ce) + copy_of_ce = copy.deepcopy(data.json_ce[v02.Event]) m = marshaller.NewHTTPMarshaller([structured.NewJSONHTTPCloudEventConverter()]) http_headers = {"content-type": "application/cloudevents+json"} event = m.FromRequest( - v01.Event(), http_headers, io.StringIO(json.dumps(data.ce)), lambda x: x.read() + v01.Event(), http_headers, io.StringIO(json.dumps(data.json_ce[v02.Event])), lambda x: x.read() ) assert event is not None assert event.Get("type") == (data.ce_type, True) diff --git a/cloudevents/tests/test_with_sanic.py b/cloudevents/tests/test_with_sanic.py index d60196e..ca6f68e 100644 --- a/cloudevents/tests/test_with_sanic.py +++ b/cloudevents/tests/test_with_sanic.py @@ -52,25 +52,25 @@ async def echo(request): def test_reusable_marshaller(): for i in range(10): _, r = app.test_client.post( - "/is-ok", headers=test_data.headers, data=test_data.body + "/is-ok", headers=test_data.headers[v02.Event], data=test_data.body ) assert r.status == 200 def test_web_app_integration(): _, r = app.test_client.post( - "/is-ok", headers=test_data.headers, data=test_data.body + "/is-ok", headers=test_data.headers[v02.Event], data=test_data.body ) assert r.status == 200 def test_web_app_echo(): - _, r = app.test_client.post("/echo", headers=test_data.headers, data=test_data.body) + _, r = app.test_client.post("/echo", headers=test_data.headers[v02.Event], data=test_data.body) assert r.status == 200 event = m.FromRequest(v02.Event(), dict(r.headers), r.body, lambda x: x) assert event is not None props = event.Properties() - for key in test_data.headers.keys(): + for key in test_data.headers[v02.Event].keys(): if key == "Content-Type": assert "contenttype" in props else: