Skip to content

0.2 force improvements #10

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

Merged
merged 6 commits into from
Jan 16, 2019
Merged
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: object) -> bool:
raise Exception("not implemented")

def can_read(self, content_type: str) -> bool:
raise Exception("not implemented")

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

def can_read(self, content_type: str) -> bool:
return True

def event_supported(self, event: object) -> bool:
return type(event) in self.SUPPORTED_VERSIONS

def read(self,
event: event_base.BaseEvent,
headers: dict, body: typing.IO,
Expand All @@ -36,11 +42,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
15 changes: 10 additions & 5 deletions cloudevents/sdk/converters/structured.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,21 @@

import typing

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


class JSONHTTPCloudEventConverter(base.Converter):

TYPE = "structured"
MIME_TYPE = "application/cloudevents+json"

def can_read(self, content_type: str) -> bool:
return content_type and content_type.startswith(self.MIME_TYPE)

def event_supported(self, event: object) -> bool:
# structured format supported by both spec 0.1 and 0.2
return True

def read(self, event: event_base.BaseEvent,
headers: dict,
Expand All @@ -33,10 +40,8 @@ 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)
http_headers = {'content-type': self.MIME_TYPE}
return http_headers, event.MarshalJSON(data_marshaller)


def NewJSONHTTPCloudEventConverter() -> JSONHTTPCloudEventConverter:
Expand Down
46 changes: 25 additions & 21 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,43 +117,47 @@ 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)
self.Set(name, value)

def UnmarshalBinary(self, headers: dict, body: typing.IO,
data_unmarshaller: typing.Callable):
props = self.Properties(with_nullable=True)
exts = props.get("extensions")
for key in props:
formatted_key = "ce-{0}".format(key)
if key != "extensions":
self.Set(key, headers.get("ce-{0}".format(key)))
if formatted_key in headers:
del headers[formatted_key]

# rest of headers suppose to an extension?
exts.update(**headers)
self.Set("extensions", exts)
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)
elif header.startswith("ce-"):
self.Set(header[3:], value)

self.Set("data", data_unmarshaller(body))

def MarshalBinary(self) -> (dict, object):
def MarshalBinary(
self, data_marshaller: typing.Callable) -> (dict, object):
headers = {}
if self.ContentType():
headers["content-type"] = self.ContentType()
props = self.Properties()
for key, value in props.items():
if key not in ["data", "extensions"]:
if key not in ["data", "extensions", "contenttype"]:
if value is not None:
headers["ce-{0}".format(key)] = value

exts = props.get("extensions")
if len(exts) > 0:
headers.update(**exts)
for key, value in props.get("extensions"):
headers["ce-{0}".format(key)] = value

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))
25 changes: 20 additions & 5 deletions cloudevents/sdk/marshaller.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def __init__(self, converters: typing.List[base.Converter]):
:param converters: a list of HTTP-to-CloudEvent-to-HTTP constructors
:type converters: typing.List[base.Converter]
"""
self.__converters = {c.TYPE: c for c in converters}
self.__converters = (c for c in converters)
self.__converters_by_type = {c.TYPE: c for c in converters}

def FromRequest(self, event: event_base.BaseEvent,
headers: dict,
Expand All @@ -55,8 +56,19 @@ def FromRequest(self, event: event_base.BaseEvent,
:return: a CloudEvent
:rtype: event_base.BaseEvent
"""
for _, cnvrtr in self.__converters.items():
return cnvrtr.read(event, headers, body, data_unmarshaller)
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:
if cnvrtr.can_read(content_type) and cnvrtr.event_supported(event):
return cnvrtr.read(event, headers, body, data_unmarshaller)

raise exceptions.UnsupportedEventConverter(
"No registered marshaller for {0} in {1}".format(
content_type, self.__converters))

def ToRequest(self, event: event_base.BaseEvent,
converter_type: str,
Expand All @@ -72,8 +84,11 @@ def ToRequest(self, event: event_base.BaseEvent,
:return: dict of HTTP headers and stream of HTTP request body
:rtype: tuple
"""
if converter_type in self.__converters:
cnvrtr = self.__converters.get(converter_type)
if not isinstance(data_marshaller, typing.Callable):
raise exceptions.InvalidDataMarshaller()

if converter_type in self.__converters_by_type:
cnvrtr = self.__converters_by_type[converter_type]
return cnvrtr.write(event, data_marshaller)

raise exceptions.NoSuchConverter(converter_type)
Expand Down
Loading