Skip to content

Commit 390f594

Browse files
authored
v1.2.0-stable (#122)
* from_http bug None and non dict data bug fixes (#119) * resolving from_http bugs Signed-off-by: Curtis Mason <[email protected]> * resolved from_http bugs Signed-off-by: Curtis Mason <[email protected]> * nit fix Signed-off-by: Curtis Mason <[email protected]> * Exceptions general class (#120) * More edgecase testing Signed-off-by: Curtis Mason <[email protected]> * Tested empty object edge cases Signed-off-by: Curtis Mason <[email protected]> * test-coverage Signed-off-by: Curtis Mason <[email protected]> * Changelog update (#121) Signed-off-by: Curtis Mason <[email protected]>
1 parent 14c7618 commit 390f594

File tree

10 files changed

+253
-45
lines changed

10 files changed

+253
-45
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [1.2.0]
8+
### Added
9+
- Added GenericException, DataMarshallingError and DataUnmarshallingError ([#120])
10+
711
## [1.1.0]
812
### Changed
913
- Changed from_http to now expect headers argument before data ([#110])
1014
- Renamed exception names ([#111])
1115

16+
### Fixed
17+
- Fixed from_http bugs with data of type None, or not dict-like ([#119])
18+
1219
### Deprecated
1320
- Renamed to_binary_http and to_structured_http. ([#108])
1421

@@ -105,3 +112,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
105112
[#108]: https://github.com/cloudevents/sdk-python/pull/108
106113
[#110]: https://github.com/cloudevents/sdk-python/pull/110
107114
[#111]: https://github.com/cloudevents/sdk-python/pull/111
115+
[#119]: https://github.com/cloudevents/sdk-python/pull/119
116+
[#120]: https://github.com/cloudevents/sdk-python/pull/120

cloudevents/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.1.0"
1+
__version__ = "1.2.0"

cloudevents/exceptions.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,29 @@
1111
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
14-
class MissingRequiredFields(Exception):
14+
class GenericException(Exception):
1515
pass
1616

1717

18-
class InvalidRequiredFields(Exception):
18+
class MissingRequiredFields(GenericException):
1919
pass
2020

2121

22-
class InvalidStructuredJSON(Exception):
22+
class InvalidRequiredFields(GenericException):
2323
pass
2424

2525

26-
class InvalidHeadersFormat(Exception):
26+
class InvalidStructuredJSON(GenericException):
27+
pass
28+
29+
30+
class InvalidHeadersFormat(GenericException):
31+
pass
32+
33+
34+
class DataMarshallerError(GenericException):
35+
pass
36+
37+
38+
class DataUnmarshallerError(GenericException):
2739
pass

cloudevents/http/event.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,14 @@ def __init__(
5959

6060
if self._attributes["specversion"] not in _required_by_version:
6161
raise cloud_exceptions.MissingRequiredFields(
62-
f"Invalid specversion: {self._attributes['specversion']}. "
62+
f"Invalid specversion: {self._attributes['specversion']}"
6363
)
6464
# There is no good way to default 'source' and 'type', so this
6565
# checks for those (or any new required attributes).
6666
required_set = _required_by_version[self._attributes["specversion"]]
6767
if not required_set <= self._attributes.keys():
6868
raise cloud_exceptions.MissingRequiredFields(
69-
f"Missing required keys: {required_set - self._attributes.keys()}. "
69+
f"Missing required keys: {required_set - self._attributes.keys()}"
7070
)
7171

7272
def __eq__(self, other):

cloudevents/http/http_methods.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,21 @@ def from_http(
2020
Unwrap a CloudEvent (binary or structured) from an HTTP request.
2121
:param headers: the HTTP headers
2222
:type headers: typing.Dict[str, str]
23-
:param data: the HTTP request body
23+
:param data: the HTTP request body. If set to None, "" or b'', the returned
24+
event's data field will be set to None
2425
:type data: typing.IO
2526
:param data_unmarshaller: Callable function to map data to a python object
2627
e.g. lambda x: x or lambda x: json.loads(x)
2728
:type data_unmarshaller: types.UnmarshallerType
2829
"""
29-
if data is None:
30+
if data is None or data == b"":
31+
# Empty string will cause data to be marshalled into None
3032
data = ""
3133

3234
if not isinstance(data, (str, bytes, bytearray)):
3335
raise cloud_exceptions.InvalidStructuredJSON(
3436
"Expected json of type (str, bytes, bytearray), "
35-
f"but instead found {type(data)}. "
37+
f"but instead found type {type(data)}"
3638
)
3739

3840
headers = {key.lower(): value for key, value in headers.items()}
@@ -47,22 +49,28 @@ def from_http(
4749
try:
4850
raw_ce = json.loads(data)
4951
except json.decoder.JSONDecodeError:
50-
raise cloud_exceptions.InvalidStructuredJSON(
51-
"Failed to read fields from structured event. "
52-
f"The following can not be parsed as json: {data}. "
52+
raise cloud_exceptions.MissingRequiredFields(
53+
"Failed to read specversion from both headers and data. "
54+
f"The following can not be parsed as json: {data}"
55+
)
56+
if hasattr(raw_ce, "get"):
57+
specversion = raw_ce.get("specversion", None)
58+
else:
59+
raise cloud_exceptions.MissingRequiredFields(
60+
"Failed to read specversion from both headers and data. "
61+
f"The following deserialized data has no 'get' method: {raw_ce}"
5362
)
54-
specversion = raw_ce.get("specversion", None)
5563

5664
if specversion is None:
5765
raise cloud_exceptions.MissingRequiredFields(
58-
"Failed to find specversion in HTTP request. "
66+
"Failed to find specversion in HTTP request"
5967
)
6068

6169
event_handler = _obj_by_version.get(specversion, None)
6270

6371
if event_handler is None:
6472
raise cloud_exceptions.InvalidRequiredFields(
65-
f"Found invalid specversion {specversion}. "
73+
f"Found invalid specversion {specversion}"
6674
)
6775

6876
event = marshall.FromRequest(
@@ -73,7 +81,13 @@ def from_http(
7381
attrs.pop("extensions", None)
7482
attrs.update(**event.extensions)
7583

76-
return CloudEvent(attrs, event.data)
84+
if event.data == "" or event.data == b"":
85+
# TODO: Check binary unmarshallers to debug why setting data to ""
86+
# returns an event with data set to None, but structured will return ""
87+
data = None
88+
else:
89+
data = event.data
90+
return CloudEvent(attrs, data)
7791

7892

7993
def _to_http(
@@ -96,7 +110,7 @@ def _to_http(
96110

97111
if event._attributes["specversion"] not in _obj_by_version:
98112
raise cloud_exceptions.InvalidRequiredFields(
99-
f"Unsupported specversion: {event._attributes['specversion']}. "
113+
f"Unsupported specversion: {event._attributes['specversion']}"
100114
)
101115

102116
event_handler = _obj_by_version[event._attributes["specversion"]]()

cloudevents/http/util.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44

55
def default_marshaller(content: any):
6-
if content is None or len(content) == 0:
6+
if content is None:
77
return None
88
try:
99
return json.dumps(content)
@@ -12,7 +12,7 @@ def default_marshaller(content: any):
1212

1313

1414
def _json_or_string(content: typing.Union[str, bytes]):
15-
if content is None or len(content) == 0:
15+
if content is None:
1616
return None
1717
try:
1818
return json.loads(content)

cloudevents/sdk/event/base.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,14 @@ def MarshalJSON(self, data_marshaller: types.MarshallerType) -> str:
201201
data_marshaller = lambda x: x # noqa: E731
202202
props = self.Properties()
203203
if "data" in props:
204-
data = data_marshaller(props.pop("data"))
204+
data = props.pop("data")
205+
try:
206+
data = data_marshaller(data)
207+
except Exception as e:
208+
raise cloud_exceptions.DataMarshallerError(
209+
"Failed to marshall data with error: "
210+
f"{type(e).__name__}('{e}')"
211+
)
205212
if isinstance(data, (bytes, bytes, memoryview)):
206213
props["data_base64"] = base64.b64encode(data).decode("ascii")
207214
else:
@@ -225,14 +232,23 @@ def UnmarshalJSON(
225232
)
226233

227234
for name, value in raw_ce.items():
235+
decoder = lambda x: x
228236
if name == "data":
229237
# Use the user-provided serializer, which may have customized
230238
# JSON decoding
231-
value = data_unmarshaller(json.dumps(value))
239+
decoder = lambda v: data_unmarshaller(json.dumps(v))
232240
if name == "data_base64":
233-
value = data_unmarshaller(base64.b64decode(value))
241+
decoder = lambda v: data_unmarshaller(base64.b64decode(v))
234242
name = "data"
235-
self.Set(name, value)
243+
244+
try:
245+
set_value = decoder(value)
246+
except Exception as e:
247+
raise cloud_exceptions.DataUnmarshallerError(
248+
"Failed to unmarshall data with error: "
249+
f"{type(e).__name__}('{e}')"
250+
)
251+
self.Set(name, set_value)
236252

237253
def UnmarshalBinary(
238254
self,
@@ -256,7 +272,15 @@ def UnmarshalBinary(
256272
self.SetContentType(value)
257273
elif header.startswith("ce-"):
258274
self.Set(header[3:], value)
259-
self.Set("data", data_unmarshaller(body))
275+
276+
try:
277+
raw_ce = data_unmarshaller(body)
278+
except Exception as e:
279+
raise cloud_exceptions.DataUnmarshallerError(
280+
"Failed to unmarshall data with error: "
281+
f"{type(e).__name__}('{e}')"
282+
)
283+
self.Set("data", raw_ce)
260284

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

278302
data, _ = self.Get("data")
279-
data = data_marshaller(data)
303+
try:
304+
data = data_marshaller(data)
305+
except Exception as e:
306+
raise cloud_exceptions.DataMarshallerError(
307+
"Failed to marshall data with error: "
308+
f"{type(e).__name__}('{e}')"
309+
)
280310
if isinstance(data, str): # Convenience method for json.dumps
281311
data = data.encode("utf-8")
282312
return headers, data

cloudevents/tests/test_http_cloudevent.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import cloudevents.exceptions as cloud_exceptions
44
from cloudevents.http import CloudEvent
5+
from cloudevents.http.util import _json_or_string
56

67

78
@pytest.mark.parametrize("specversion", ["0.3", "1.0"])
@@ -75,19 +76,19 @@ def test_http_cloudevent_mutates_equality(specversion):
7576
def test_cloudevent_missing_specversion():
7677
attributes = {"specversion": "0.2", "source": "s", "type": "t"}
7778
with pytest.raises(cloud_exceptions.MissingRequiredFields) as e:
78-
event = CloudEvent(attributes, None)
79+
_ = CloudEvent(attributes, None)
7980
assert "Invalid specversion: 0.2" in str(e.value)
8081

8182

8283
def test_cloudevent_missing_minimal_required_fields():
8384
attributes = {"type": "t"}
8485
with pytest.raises(cloud_exceptions.MissingRequiredFields) as e:
85-
event = CloudEvent(attributes, None)
86+
_ = CloudEvent(attributes, None)
8687
assert f"Missing required keys: {set(['source'])}" in str(e.value)
8788

8889
attributes = {"source": "s"}
8990
with pytest.raises(cloud_exceptions.MissingRequiredFields) as e:
90-
event = CloudEvent(attributes, None)
91+
_ = CloudEvent(attributes, None)
9192
assert f"Missing required keys: {set(['type'])}" in str(e.value)
9293

9394

@@ -114,3 +115,7 @@ def test_cloudevent_general_overrides():
114115
assert attribute in event
115116
del event[attribute]
116117
assert len(event) == 0
118+
119+
120+
def test_none_json_or_string():
121+
assert _json_or_string(None) is None

0 commit comments

Comments
 (0)