Skip to content

Consolidating return types #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,69 @@ headers, body = m.ToRequest(event, converters.TypeStructured, lambda x: x)

```

## HOWTOs with various Python HTTP frameworks

In this topic you'd find various example how to integrate an SDK with various HTTP frameworks.

### Python requests

One of popular framework is [0.2-force-improvements](http://docs.python-requests.org/en/master/).


#### CloudEvent to request

The code below shows how integrate both libraries in order to convert a CloudEvent into an HTTP request:

```python
def run_binary(event, url):
binary_headers, binary_data = http_marshaller.ToRequest(
event, converters.TypeBinary, json.dumps)

print("binary CloudEvent")
for k, v in binary_headers.items():
print("{0}: {1}\r\n".format(k, v))
print(binary_data.getvalue())
response = requests.post(url,
headers=binary_headers,
data=binary_data.getvalue())
response.raise_for_status()


def run_structured(event, url):
structured_headers, structured_data = http_marshaller.ToRequest(
event, converters.TypeStructured, json.dumps
)
print("structured CloudEvent")
print(structured_data.getvalue())

response = requests.post(url,
headers=structured_headers,
data=structured_data.getvalue())
response.raise_for_status()

```

Complete example of turning a CloudEvent into a request you can find [here](samples/python-requests/cloudevent_to_request.py).

#### Request to CloudEvent

The code below shows how integrate both libraries in order to create a CloudEvent from an HTTP request:
```python
response = requests.get(url)
response.raise_for_status()
headers = response.headers
data = io.BytesIO(response.content)
event = v02.Event()
http_marshaller = marshaller.NewDefaultHTTPMarshaller()
event = http_marshaller.FromRequest(
event, headers, data, json.load)

```
Complete example of turning a CloudEvent into a request you can find [here](samples/python-requests/request_to_cloudevent.py).


## SDK versioning

The goal of this package is to provide support for all released versions of CloudEvents, ideally while maintaining
the same API. It will use semantic versioning with following rules:
* MAJOR version increments when backwards incompatible changes is introduced.
Expand Down
6 changes: 6 additions & 0 deletions cloudevents/sdk/converters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ def read(self, event, headers: dict, body: typing.IO,
data_unmarshaller: typing.Callable) -> base.BaseEvent:
raise Exception("not implemented")

def event_supported(self, event):
raise Exception("not implemented")

def can_read(self, content_type):
raise Exception("not implemented")

def write(self, event: base.BaseEvent,
data_marshaller: typing.Callable) -> (dict, typing.IO):
raise Exception("not implemented")
13 changes: 8 additions & 5 deletions cloudevents/sdk/converters/binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ class BinaryHTTPCloudEventConverter(base.Converter):
TYPE = "binary"
SUPPORTED_VERSIONS = [v02.Event, ]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


def can_read(self, content_type):
return True

def event_supported(self, event):
if type(event) not in self.SUPPORTED_VERSIONS:
raise exceptions.UnsupportedEvent(type(event))

def read(self,
event: event_base.BaseEvent,
headers: dict, body: typing.IO,
Expand All @@ -36,11 +43,7 @@ def read(self,

def write(self, event: event_base.BaseEvent,
data_marshaller: typing.Callable) -> (dict, typing.IO):
if not isinstance(data_marshaller, typing.Callable):
raise exceptions.InvalidDataMarshaller()

hs, data = event.MarshalBinary()
return hs, data_marshaller(data)
return event.MarshalBinary(data_marshaller)


def NewBinaryHTTPCloudEventConverter() -> BinaryHTTPCloudEventConverter:
Expand Down
11 changes: 7 additions & 4 deletions cloudevents/sdk/converters/structured.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

import typing

from cloudevents.sdk import exceptions
from cloudevents.sdk.converters import base
from cloudevents.sdk.event import base as event_base

Expand All @@ -23,6 +22,13 @@ class JSONHTTPCloudEventConverter(base.Converter):

TYPE = "structured"

def can_read(self, content_type):
return content_type == "application/cloudevents+json"

def event_supported(self, event):
# structured format supported by both spec 0.1 and 0.2
pass

def read(self, event: event_base.BaseEvent,
headers: dict,
body: typing.IO,
Expand All @@ -33,9 +39,6 @@ def read(self, event: event_base.BaseEvent,
def write(self,
event: event_base.BaseEvent,
data_marshaller: typing.Callable) -> (dict, typing.IO):
if not isinstance(data_marshaller, typing.Callable):
raise exceptions.InvalidDataMarshaller()

return {}, event.MarshalJSON(data_marshaller)


Expand Down
12 changes: 7 additions & 5 deletions cloudevents/sdk/event/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# under the License.

import io
import ujson
import json
import typing


Expand Down Expand Up @@ -117,11 +117,11 @@ def Set(self, key: str, value: object):
def MarshalJSON(self, data_marshaller: typing.Callable) -> typing.IO:
props = self.Properties()
props["data"] = data_marshaller(props.get("data"))
return io.StringIO(ujson.dumps(props))
return io.BytesIO(json.dumps(props).encode("utf-8"))

def UnmarshalJSON(self, b: typing.IO,
data_unmarshaller: typing.Callable):
raw_ce = ujson.load(b)
raw_ce = json.load(b)
for name, value in raw_ce.items():
if name == "data":
value = data_unmarshaller(value)
Expand All @@ -143,7 +143,8 @@ def UnmarshalBinary(self, headers: dict, body: typing.IO,
self.Set("extensions", exts)
self.Set("data", data_unmarshaller(body))

def MarshalBinary(self) -> (dict, object):
def MarshalBinary(
self, data_marshaller: typing.Callable) -> (dict, object):
headers = {}
props = self.Properties()
for key, value in props.items():
Expand All @@ -156,4 +157,5 @@ def MarshalBinary(self) -> (dict, object):
headers.update(**exts)

data, _ = self.Get("data")
return headers, data
return headers, io.BytesIO(
str(data_marshaller(data)).encode("utf-8"))
14 changes: 14 additions & 0 deletions cloudevents/sdk/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ def __init__(self, event_class):
"'{0}'".format(event_class))


class InvalidDataUnmarshaller(Exception):

def __init__(self):
super().__init__(
"Invalid data unmarshaller, is not a callable")


class InvalidDataMarshaller(Exception):

def __init__(self):
Expand All @@ -31,3 +38,10 @@ class NoSuchConverter(Exception):
def __init__(self, converter_type):
super().__init__(
"No such converter {0}".format(converter_type))


class UnsupportedEventConverter(Exception):
def __init__(self, content_type):
super().__init__(
"Unable to identify valid event converter "
"for content-type: '{0}'".format(content_type))
15 changes: 14 additions & 1 deletion cloudevents/sdk/marshaller.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,18 @@ def FromRequest(self, event: event_base.BaseEvent,
:return: a CloudEvent
:rtype: event_base.BaseEvent
"""
if not isinstance(data_unmarshaller, typing.Callable):
raise exceptions.InvalidDataUnmarshaller()

content_type = headers.get(
"content-type", headers.get("Content-Type"))

for _, cnvrtr in self.__converters.items():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is treating self.__converters as an unordered list (despite having been passed as an ordered list on line 102. dict randomization will mean that binary mode will sometimes be selected for a structured-mode request, which will cause test flakiness. (Discovered this by accident when re-running tests after fixing a lint error.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I fixed this in #9)

return cnvrtr.read(event, headers, body, data_unmarshaller)
if cnvrtr.can_read(content_type):
cnvrtr.event_supported(event)
return cnvrtr.read(event, headers, body, data_unmarshaller)

raise exceptions.UnsupportedEventConverter(content_type)

def ToRequest(self, event: event_base.BaseEvent,
converter_type: str,
Expand All @@ -72,6 +82,9 @@ def ToRequest(self, event: event_base.BaseEvent,
:return: dict of HTTP headers and stream of HTTP request body
:rtype: tuple
"""
if not isinstance(data_marshaller, typing.Callable):
raise exceptions.InvalidDataMarshaller()

if converter_type in self.__converters:
cnvrtr = self.__converters.get(converter_type)
return cnvrtr.write(event, data_marshaller)
Expand Down
56 changes: 50 additions & 6 deletions cloudevents/tests/test_event_from_request_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
# License for the specific language governing permissions and limitations
# under the License.

import json
import pytest
import io
import ujson

from cloudevents.sdk import exceptions
from cloudevents.sdk import marshaller
Expand Down Expand Up @@ -49,7 +49,7 @@ def test_structured_converter_upstream():
event = m.FromRequest(
v02.Event(),
{"Content-Type": "application/cloudevents+json"},
io.StringIO(ujson.dumps(data.ce)),
io.StringIO(json.dumps(data.ce)),
lambda x: x.read()
)

Expand All @@ -58,7 +58,6 @@ def test_structured_converter_upstream():
assert event.Get("id") == (data.ce_id, True)


# todo: clarify whether spec 0.1 doesn't support binary format
def test_binary_converter_v01():
m = marshaller.NewHTTPMarshaller(
[
Expand All @@ -69,7 +68,20 @@ def test_binary_converter_v01():
pytest.raises(
exceptions.UnsupportedEvent,
m.FromRequest,
v01.Event, None, None, None)
v01.Event, {}, None, lambda x: x)


def test_unsupported_converter_v01():
m = marshaller.NewHTTPMarshaller(
[
structured.NewJSONHTTPCloudEventConverter()
]
)

pytest.raises(
exceptions.UnsupportedEventConverter,
m.FromRequest,
v01.Event, {}, None, lambda x: x)


def test_structured_converter_v01():
Expand All @@ -81,7 +93,7 @@ def test_structured_converter_v01():
event = m.FromRequest(
v01.Event(),
{"Content-Type": "application/cloudevents+json"},
io.StringIO(ujson.dumps(data.ce)),
io.StringIO(json.dumps(data.ce)),
lambda x: x.read()
)

Expand All @@ -96,9 +108,41 @@ def test_default_http_marshaller():
event = m.FromRequest(
v02.Event(),
{"Content-Type": "application/cloudevents+json"},
io.StringIO(ujson.dumps(data.ce)),
io.StringIO(json.dumps(data.ce)),
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)


def test_unsupported_event_configuration():
m = marshaller.NewHTTPMarshaller(
[
binary.NewBinaryHTTPCloudEventConverter()
]
)
pytest.raises(
exceptions.UnsupportedEvent,
m.FromRequest,
v01.Event(),
{"Content-Type": "application/cloudevents+json"},
io.StringIO(json.dumps(data.ce)),
lambda x: x.read()
)


def test_invalid_data_unmarshaller():
m = marshaller.NewDefaultHTTPMarshaller()
pytest.raises(
exceptions.InvalidDataUnmarshaller,
m.FromRequest,
v01.Event(), {}, None, None)


def test_invalid_data_marshaller():
m = marshaller.NewDefaultHTTPMarshaller()
pytest.raises(
exceptions.InvalidDataMarshaller,
m.ToRequest,
v01.Event(), "blah", None)
9 changes: 6 additions & 3 deletions cloudevents/tests/test_event_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.

import ujson
import io
import json

from cloudevents.sdk.event import v01
from cloudevents.sdk.event import v02
Expand Down Expand Up @@ -43,7 +44,8 @@ def test_event_pipeline_upstream():
assert "ce-id" in new_headers
assert "ce-time" in new_headers
assert "ce-contenttype" in new_headers
assert data.body == body
assert isinstance(body, io.BytesIO)
assert data.body == body.read().decode("utf-8")


def test_event_pipeline_v01():
Expand All @@ -63,7 +65,8 @@ def test_event_pipeline_v01():
)

_, body = m.ToRequest(event, converters.TypeStructured, lambda x: x)
new_headers = ujson.load(body)
assert isinstance(body, io.BytesIO)
new_headers = json.load(body)
assert new_headers is not None
assert "cloudEventsVersion" in new_headers
assert "eventType" in new_headers
Expand Down
Loading