Skip to content

Exceptions general class #120

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 4 commits into from
Aug 19, 2020
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
2 changes: 1 addition & 1 deletion cloudevents/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.1.0"
__version__ = "1.2.0"
20 changes: 16 additions & 4 deletions cloudevents/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,29 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
class MissingRequiredFields(Exception):
class GenericException(Exception):
pass


class InvalidRequiredFields(Exception):
class MissingRequiredFields(GenericException):
pass


class InvalidStructuredJSON(Exception):
class InvalidRequiredFields(GenericException):
pass


class InvalidHeadersFormat(Exception):
class InvalidStructuredJSON(GenericException):
pass


class InvalidHeadersFormat(GenericException):
pass


class DataMarshallerError(GenericException):
pass


class DataUnmarshallerError(GenericException):
pass
14 changes: 11 additions & 3 deletions cloudevents/http/http_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ def from_http(
Unwrap a CloudEvent (binary or structured) from an HTTP request.
:param headers: the HTTP headers
:type headers: typing.Dict[str, str]
:param data: the HTTP request body
:param data: the HTTP request body. If set to None, "" or b'', the returned
event's data field will be set to None
:type data: typing.IO
:param data_unmarshaller: Callable function to map data to a python object
e.g. lambda x: x or lambda x: json.loads(x)
:type data_unmarshaller: types.UnmarshallerType
"""
if data is None:
if data is None or data == b"":
# Empty string will cause data to be marshalled into None
data = ""

if not isinstance(data, (str, bytes, bytearray)):
Expand Down Expand Up @@ -79,7 +81,13 @@ def from_http(
attrs.pop("extensions", None)
attrs.update(**event.extensions)

return CloudEvent(attrs, event.data)
if event.data == "" or event.data == b"":
# TODO: Check binary unmarshallers to debug why setting data to ""
# returns an event with data set to None, but structured will return ""
data = None
else:
data = event.data
return CloudEvent(attrs, data)


def _to_http(
Expand Down
4 changes: 2 additions & 2 deletions cloudevents/http/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@


def default_marshaller(content: any):
if content is None or len(content) == 0:
if content is None:
return None
try:
return json.dumps(content)
Expand All @@ -12,7 +12,7 @@ def default_marshaller(content: any):


def _json_or_string(content: typing.Union[str, bytes]):
if content is None or len(content) == 0:
if content is None:
return None
try:
return json.loads(content)
Expand Down
42 changes: 36 additions & 6 deletions cloudevents/sdk/event/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,14 @@ def MarshalJSON(self, data_marshaller: types.MarshallerType) -> str:
data_marshaller = lambda x: x # noqa: E731
props = self.Properties()
if "data" in props:
data = data_marshaller(props.pop("data"))
data = props.pop("data")
try:
data = data_marshaller(data)
except Exception as e:
raise cloud_exceptions.DataMarshallerError(
"Failed to marshall data with error: "
f"{type(e).__name__}('{e}')"
)
if isinstance(data, (bytes, bytes, memoryview)):
props["data_base64"] = base64.b64encode(data).decode("ascii")
else:
Expand All @@ -225,14 +232,23 @@ def UnmarshalJSON(
)

for name, value in raw_ce.items():
decoder = lambda x: x
if name == "data":
# Use the user-provided serializer, which may have customized
# JSON decoding
value = data_unmarshaller(json.dumps(value))
decoder = lambda v: data_unmarshaller(json.dumps(v))
if name == "data_base64":
value = data_unmarshaller(base64.b64decode(value))
decoder = lambda v: data_unmarshaller(base64.b64decode(v))
name = "data"
self.Set(name, value)

try:
set_value = decoder(value)
except Exception as e:
raise cloud_exceptions.DataUnmarshallerError(
"Failed to unmarshall data with error: "
f"{type(e).__name__}('{e}')"
)
self.Set(name, set_value)

def UnmarshalBinary(
self,
Expand All @@ -256,7 +272,15 @@ def UnmarshalBinary(
self.SetContentType(value)
elif header.startswith("ce-"):
self.Set(header[3:], value)
self.Set("data", data_unmarshaller(body))

try:
raw_ce = data_unmarshaller(body)
except Exception as e:
raise cloud_exceptions.DataUnmarshallerError(
"Failed to unmarshall data with error: "
f"{type(e).__name__}('{e}')"
)
self.Set("data", raw_ce)

def MarshalBinary(
self, data_marshaller: types.MarshallerType
Expand All @@ -276,7 +300,13 @@ def MarshalBinary(
headers["ce-{0}".format(key)] = value

data, _ = self.Get("data")
data = data_marshaller(data)
try:
data = data_marshaller(data)
except Exception as e:
raise cloud_exceptions.DataMarshallerError(
"Failed to marshall data with error: "
f"{type(e).__name__}('{e}')"
)
if isinstance(data, str): # Convenience method for json.dumps
data = data.encode("utf-8")
return headers, data
5 changes: 5 additions & 0 deletions cloudevents/tests/test_http_cloudevent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import cloudevents.exceptions as cloud_exceptions
from cloudevents.http import CloudEvent
from cloudevents.http.util import _json_or_string


@pytest.mark.parametrize("specversion", ["0.3", "1.0"])
Expand Down Expand Up @@ -114,3 +115,7 @@ def test_cloudevent_general_overrides():
assert attribute in event
del event[attribute]
assert len(event) == 0


def test_none_json_or_string():
assert _json_or_string(None) is None
47 changes: 45 additions & 2 deletions cloudevents/tests/test_http_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,17 @@ def test_empty_data_structured_event(specversion):
"source": "<source-url>",
}

_ = from_http(
event = from_http(
{"content-type": "application/cloudevents+json"}, json.dumps(attributes)
)
assert event.data == None

attributes["data"] = ""
# Data of empty string will be marshalled into None
event = from_http(
{"content-type": "application/cloudevents+json"}, json.dumps(attributes)
)
assert event.data == None


@pytest.mark.parametrize("specversion", ["1.0", "0.3"])
Expand All @@ -302,7 +310,13 @@ def test_empty_data_binary_event(specversion):
"ce-time": "2018-10-23T12:28:22.4579346Z",
"ce-source": "<source-url>",
}
_ = from_http(headers, "")
event = from_http(headers, None)
assert event.data == None

data = ""
# Data of empty string will be marshalled into None
event = from_http(headers, data)
assert event.data == None


@pytest.mark.parametrize("specversion", ["1.0", "0.3"])
Expand Down Expand Up @@ -476,6 +490,35 @@ def test_uppercase_headers_with_none_data_binary():
assert new_data == None


def test_generic_exception():
headers = {"Content-Type": "application/cloudevents+json"}
data = json.dumps(
{
"specversion": "1.0",
"source": "s",
"type": "t",
"id": "1234-1234-1234",
"data": "",
}
)
with pytest.raises(cloud_exceptions.GenericException) as e:
from_http({}, None)
e.errisinstance(cloud_exceptions.MissingRequiredFields)

with pytest.raises(cloud_exceptions.GenericException) as e:
from_http({}, 123)
e.errisinstance(cloud_exceptions.InvalidStructuredJSON)

with pytest.raises(cloud_exceptions.GenericException) as e:
from_http(headers, data, data_unmarshaller=lambda x: 1 / 0)
e.errisinstance(cloud_exceptions.DataUnmarshallerError)

with pytest.raises(cloud_exceptions.GenericException) as e:
event = from_http(headers, data)
to_binary(event, data_marshaller=lambda x: 1 / 0)
e.errisinstance(cloud_exceptions.DataMarshallerError)


def test_non_dict_data_no_headers_bug():
# Test for issue #116
headers = {"Content-Type": "application/cloudevents+json"}
Expand Down
85 changes: 82 additions & 3 deletions cloudevents/tests/test_marshaller.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@
# License for the specific language governing permissions and limitations
# under the License.

import json

import pytest

import cloudevents.exceptions as cloud_exceptions
from cloudevents.http import CloudEvent, from_http, to_binary, to_structured
from cloudevents.sdk import converters, exceptions, marshaller
from cloudevents.sdk.converters import binary, structured
from cloudevents.sdk.event import v1


@pytest.fixture
def headers():
def binary_headers():
return {
"ce-specversion": "1.0",
"ce-source": "1.0",
Expand All @@ -29,6 +33,19 @@ def headers():
}


@pytest.fixture
def structured_data():
return json.dumps(
{
"specversion": "1.0",
"source": "pytest",
"type": "com.pytest.test",
"id": "1234-1234-1234",
"data": "test",
}
)


def test_from_request_wrong_unmarshaller():
with pytest.raises(exceptions.InvalidDataUnmarshaller):
m = marshaller.NewDefaultHTTPMarshaller()
Expand All @@ -41,7 +58,7 @@ def test_to_request_wrong_marshaller():
_ = m.ToRequest(v1.Event(), data_marshaller="")


def test_from_request_cannot_read(headers):
def test_from_request_cannot_read(binary_headers):
with pytest.raises(exceptions.UnsupportedEventConverter):
m = marshaller.HTTPMarshaller(
[binary.NewBinaryHTTPCloudEventConverter(),]
Expand All @@ -52,7 +69,7 @@ def test_from_request_cannot_read(headers):
m = marshaller.HTTPMarshaller(
[structured.NewJSONHTTPCloudEventConverter()]
)
m.FromRequest(v1.Event(), headers, "")
m.FromRequest(v1.Event(), binary_headers, "")


def test_to_request_invalid_converter():
Expand All @@ -61,3 +78,65 @@ def test_to_request_invalid_converter():
[structured.NewJSONHTTPCloudEventConverter()]
)
m.ToRequest(v1.Event(), "")


def test_http_data_unmarshaller_exceptions(binary_headers, structured_data):
# binary
with pytest.raises(cloud_exceptions.DataUnmarshallerError) as e:
from_http(binary_headers, None, data_unmarshaller=lambda x: 1 / 0)
assert (
"Failed to unmarshall data with error: "
"ZeroDivisionError('division by zero')" in str(e.value)
)

# structured
headers = {"Content-Type": "application/cloudevents+json"}
with pytest.raises(cloud_exceptions.DataUnmarshallerError) as e:
from_http(headers, structured_data, data_unmarshaller=lambda x: 1 / 0)
assert (
"Failed to unmarshall data with error: "
"ZeroDivisionError('division by zero')" in str(e.value)
)


def test_http_data_marshaller_exception(binary_headers, structured_data):
# binary
event = from_http(binary_headers, None)
with pytest.raises(cloud_exceptions.DataMarshallerError) as e:
to_binary(event, data_marshaller=lambda x: 1 / 0)
assert (
"Failed to marshall data with error: "
"ZeroDivisionError('division by zero')" in str(e.value)
)

# structured
headers = {"Content-Type": "application/cloudevents+json"}

event = from_http(headers, structured_data)
with pytest.raises(cloud_exceptions.DataMarshallerError) as e:
to_structured(event, data_marshaller=lambda x: 1 / 0)
assert (
"Failed to marshall data with error: "
"ZeroDivisionError('division by zero')" in str(e.value)
)


@pytest.mark.parametrize("test_data", [[], {}, (), "", b"", None])
def test_known_empty_edge_cases(binary_headers, test_data):
expect_data = test_data
if test_data in ["", b""]:
expect_data = None
elif test_data == ():
# json.dumps(()) outputs '[]' hence list not tuple check
expect_data = []

# Remove ce- prefix
headers = {key[3:]: value for key, value in binary_headers.items()}

# binary
event = from_http(*to_binary(CloudEvent(headers, test_data)))
assert event.data == expect_data

# structured
event = from_http(*to_structured(CloudEvent(headers, test_data)))
assert event.data == expect_data