Added Cloudevents V0.3 and V1 implementations (#22)

* Added v1 and v03 specs

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Added v1 and v03 specs implementations

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* linter

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* linter 2

Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>

* Add changelog entry

Signed-off-by: Dustin Ingram <di@users.noreply.github.com>

Co-authored-by: Dustin Ingram <di@users.noreply.github.com>
This commit is contained in:
Francesco Guardiani 2020-04-24 16:57:41 +02:00 committed by GitHub
parent b7ad8c2fbb
commit bcacf3391a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 328 additions and 69 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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,
},
}

View File

@ -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(),
)

View File

@ -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)

View File

@ -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)

View File

@ -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: