From 14c76188d16909613f5de1468348da1a1973104b Mon Sep 17 00:00:00 2001 From: Curtis Mason <31265687+cumason123@users.noreply.github.com> Date: Tue, 18 Aug 2020 10:49:02 -0400 Subject: [PATCH] V1.1.0 dev (#114) * 100% test-coverage rule added to tox (#109) * version bump Signed-off-by: Curtis Mason * adding tests for marshaller Signed-off-by: Curtis Mason * marshaller 100% test-coverage Signed-off-by: Curtis Mason * bricked some tests Signed-off-by: Curtis Mason * additional error handling Signed-off-by: Curtis Mason * 100% test-coverage Signed-off-by: Curtis Mason * handles empty data and capitalized headers Signed-off-by: Curtis Mason * 1.1.0 version bump Signed-off-by: Curtis Mason * Removed _http suffix from http_methods (#108) * Removed _http suffix from http_methods to_binary_http renamed to_binary, and to_structured_http renamed to_structured. These functions are inside of cloudevents.http thus the _http part should be implicitly understood. Signed-off-by: Curtis Mason * version bump Signed-off-by: Curtis Mason * deprecated instead of removal Signed-off-by: Curtis Mason * Update setup.py Co-authored-by: Dustin Ingram Signed-off-by: Curtis Mason * 1.1.0 version bump Signed-off-by: Curtis Mason Co-authored-by: Dustin Ingram * swapped args for from_http (#110) Signed-off-by: Curtis Mason * exception names shortened (#111) * exception names shortened Signed-off-by: Curtis Mason * to_structured documentation Signed-off-by: Curtis Mason * adjusted readme and changelog (#113) * adjusted readme and changelog Signed-off-by: Curtis Mason * readme adjustment Signed-off-by: Curtis Mason * structured content mode Signed-off-by: Curtis Mason Co-authored-by: Dustin Ingram --- CHANGELOG.md | 13 +- README.md | 23 ++-- cloudevents/__init__.py | 2 +- cloudevents/exceptions.py | 12 +- cloudevents/http/__init__.py | 2 + cloudevents/http/event.py | 8 +- cloudevents/http/http_methods.py | 60 +++++++-- cloudevents/http/json_methods.py | 6 +- cloudevents/http/util.py | 2 +- cloudevents/sdk/event/base.py | 6 +- cloudevents/sdk/event/v03.py | 24 +++- cloudevents/sdk/event/v1.py | 16 +++ cloudevents/tests/test_base_events.py | 33 +++++ cloudevents/tests/test_converters.py | 41 ++++++ cloudevents/tests/test_data_encaps_refs.py | 1 - .../tests/test_deprecated_functions.py | 37 ++++++ cloudevents/tests/test_event_extensions.py | 15 +-- .../tests/test_event_to_request_converter.py | 1 - ...t_overrides.py => test_http_cloudevent.py} | 45 +++++++ cloudevents/tests/test_http_events.py | 120 ++++++++++++++---- cloudevents/tests/test_marshaller.py | 63 +++++++++ cloudevents/tests/test_options.py | 36 ++++++ cloudevents/tests/test_v03_event.py | 64 ++++++++++ cloudevents/tests/test_v1_event.py | 53 ++++++++ samples/http-image-cloudevents/client.py | 8 +- .../image_sample_server.py | 2 +- .../image_sample_test.py | 21 ++- samples/http-json-cloudevents/client.py | 6 +- .../json_sample_server.py | 2 +- .../http-json-cloudevents/json_sample_test.py | 6 +- setup.py | 1 + tox.ini | 2 +- 32 files changed, 627 insertions(+), 104 deletions(-) create mode 100644 cloudevents/tests/test_base_events.py create mode 100644 cloudevents/tests/test_converters.py create mode 100644 cloudevents/tests/test_deprecated_functions.py rename cloudevents/tests/{test_http_cloudevent_overrides.py => test_http_cloudevent.py} (61%) create mode 100644 cloudevents/tests/test_marshaller.py create mode 100644 cloudevents/tests/test_options.py create mode 100644 cloudevents/tests/test_v03_event.py create mode 100644 cloudevents/tests/test_v1_event.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7901b02..c24d9ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] +### Changed +- Changed from_http to now expect headers argument before data ([#110]) +- Renamed exception names ([#111]) + +### Deprecated +- Renamed to_binary_http and to_structured_http. ([#108]) + ## [1.0.1] ### Added - CloudEvent exceptions and event type checking in http module ([#96]) @@ -93,4 +101,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#71]: https://github.com/cloudevents/sdk-python/pull/71 [#72]: https://github.com/cloudevents/sdk-python/pull/72 [#96]: https://github.com/cloudevents/sdk-python/pull/96 -[#98]: https://github.com/cloudevents/sdk-python/pull/98 \ No newline at end of file +[#98]: https://github.com/cloudevents/sdk-python/pull/98 +[#108]: https://github.com/cloudevents/sdk-python/pull/108 +[#110]: https://github.com/cloudevents/sdk-python/pull/110 +[#111]: https://github.com/cloudevents/sdk-python/pull/111 diff --git a/README.md b/README.md index 22539ac..e41b7a3 100644 --- a/README.md +++ b/README.md @@ -24,19 +24,20 @@ Below we will provide samples on how to send cloudevents using the popular ### Binary HTTP CloudEvent ```python -from cloudevents.http import CloudEvent, to_binary_http +from cloudevents.http import CloudEvent, to_binary import requests - -# This data defines a binary cloudevent +# Create a CloudEvent +# - The CloudEvent "id" is generated if omitted. "specversion" defaults to "1.0". attributes = { "type": "com.example.sampletype1", "source": "https://example.com/event-producer", } data = {"message": "Hello World!"} - event = CloudEvent(attributes, data) -headers, body = to_binary_http(event) + +# Creates the HTTP request representation of the CloudEvent in binary content mode +headers, body = to_binary(event) # POST requests.post("", data=body, headers=headers) @@ -45,18 +46,20 @@ requests.post("", data=body, headers=headers) ### Structured HTTP CloudEvent ```python -from cloudevents.http import CloudEvent, to_structured_http +from cloudevents.http import CloudEvent, to_structured import requests - -# This data defines a structured cloudevent +# Create a CloudEvent +# - The CloudEvent "id" is generated if omitted. "specversion" defaults to "1.0". attributes = { "type": "com.example.sampletype2", "source": "https://example.com/event-producer", } data = {"message": "Hello World!"} event = CloudEvent(attributes, data) -headers, body = to_structured_http(event) + +# Creates the HTTP request representation of the CloudEvent in structured content mode +headers, body = to_structured(event) # POST requests.post("", data=body, headers=headers) @@ -81,7 +84,7 @@ app = Flask(__name__) @app.route("/", methods=["POST"]) def home(): # create a CloudEvent - event = from_http(request.get_data(), request.headers) + event = from_http(request.headers, request.get_data()) # you can access cloudevent fields as seen below print( diff --git a/cloudevents/__init__.py b/cloudevents/__init__.py index 5c4105c..6849410 100644 --- a/cloudevents/__init__.py +++ b/cloudevents/__init__.py @@ -1 +1 @@ -__version__ = "1.0.1" +__version__ = "1.1.0" diff --git a/cloudevents/exceptions.py b/cloudevents/exceptions.py index 5d2e191..776e58a 100644 --- a/cloudevents/exceptions.py +++ b/cloudevents/exceptions.py @@ -11,9 +11,17 @@ # 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 CloudEventMissingRequiredFields(Exception): +class MissingRequiredFields(Exception): pass -class CloudEventTypeErrorRequiredFields(Exception): +class InvalidRequiredFields(Exception): + pass + + +class InvalidStructuredJSON(Exception): + pass + + +class InvalidHeadersFormat(Exception): pass diff --git a/cloudevents/http/__init__.py b/cloudevents/http/__init__.py index d7c62ec..0491860 100644 --- a/cloudevents/http/__init__.py +++ b/cloudevents/http/__init__.py @@ -18,7 +18,9 @@ from cloudevents.http.event import CloudEvent from cloudevents.http.event_type import is_binary, is_structured from cloudevents.http.http_methods import ( from_http, + to_binary, to_binary_http, + to_structured, to_structured_http, ) from cloudevents.http.json_methods import from_json, to_json diff --git a/cloudevents/http/event.py b/cloudevents/http/event.py index 7354ae3..5cb2efb 100644 --- a/cloudevents/http/event.py +++ b/cloudevents/http/event.py @@ -58,15 +58,15 @@ class CloudEvent: ).isoformat() if self._attributes["specversion"] not in _required_by_version: - raise cloud_exceptions.CloudEventMissingRequiredFields( - f"Invalid specversion: {self._attributes['specversion']}" + raise cloud_exceptions.MissingRequiredFields( + f"Invalid specversion: {self._attributes['specversion']}. " ) # There is no good way to default 'source' and 'type', so this # checks for those (or any new required attributes). required_set = _required_by_version[self._attributes["specversion"]] if not required_set <= self._attributes.keys(): - raise cloud_exceptions.CloudEventMissingRequiredFields( - f"Missing required keys: {required_set - attributes.keys()}" + raise cloud_exceptions.MissingRequiredFields( + f"Missing required keys: {required_set - self._attributes.keys()}. " ) def __eq__(self, other): diff --git a/cloudevents/http/http_methods.py b/cloudevents/http/http_methods.py index 6f7b68d..ef186de 100644 --- a/cloudevents/http/http_methods.py +++ b/cloudevents/http/http_methods.py @@ -1,6 +1,8 @@ import json import typing +from deprecation import deprecated + import cloudevents.exceptions as cloud_exceptions from cloudevents.http.event import CloudEvent from cloudevents.http.event_type import is_binary, is_structured @@ -10,20 +12,30 @@ from cloudevents.sdk import converters, marshaller, types def from_http( - data: typing.Union[str, bytes], headers: typing.Dict[str, str], + data: typing.Union[str, bytes, None], data_unmarshaller: types.UnmarshallerType = None, ): """ Unwrap a CloudEvent (binary or structured) from an HTTP request. - :param data: the HTTP request body - :type data: typing.IO :param headers: the HTTP headers :type headers: typing.Dict[str, str] + :param data: the HTTP request body + :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: + data = "" + + if not isinstance(data, (str, bytes, bytearray)): + raise cloud_exceptions.InvalidStructuredJSON( + "Expected json of type (str, bytes, bytearray), " + f"but instead found {type(data)}. " + ) + + headers = {key.lower(): value for key, value in headers.items()} if data_unmarshaller is None: data_unmarshaller = _json_or_string @@ -32,19 +44,25 @@ def from_http( if is_binary(headers): specversion = headers.get("ce-specversion", None) else: - raw_ce = json.loads(data) + try: + raw_ce = json.loads(data) + except json.decoder.JSONDecodeError: + raise cloud_exceptions.InvalidStructuredJSON( + "Failed to read fields from structured event. " + f"The following can not be parsed as json: {data}. " + ) specversion = raw_ce.get("specversion", None) if specversion is None: - raise cloud_exceptions.CloudEventMissingRequiredFields( - "could not find specversion in HTTP request" + raise cloud_exceptions.MissingRequiredFields( + "Failed to find specversion in HTTP request. " ) event_handler = _obj_by_version.get(specversion, None) if event_handler is None: - raise cloud_exceptions.CloudEventTypeErrorRequiredFields( - f"found invalid specversion {specversion}" + raise cloud_exceptions.InvalidRequiredFields( + f"Found invalid specversion {specversion}. " ) event = marshall.FromRequest( @@ -77,8 +95,8 @@ def _to_http( data_marshaller = _marshaller_by_format[format] if event._attributes["specversion"] not in _obj_by_version: - raise cloud_exceptions.CloudEventTypeErrorRequiredFields( - f"Unsupported specversion: {event._attributes['specversion']}" + raise cloud_exceptions.InvalidRequiredFields( + f"Unsupported specversion: {event._attributes['specversion']}. " ) event_handler = _obj_by_version[event._attributes["specversion"]]() @@ -91,11 +109,13 @@ def _to_http( ) -def to_structured_http( +def to_structured( event: CloudEvent, data_marshaller: types.MarshallerType = None, ) -> (dict, typing.Union[bytes, str]): """ - Returns a tuple of HTTP headers/body dicts representing this cloudevent + Returns a tuple of HTTP headers/body dicts representing this cloudevent. If + event.data is a byte object, body will have a data_base64 field instead of + data. :param event: CloudEvent to cast into http data :type event: CloudEvent @@ -107,7 +127,7 @@ def to_structured_http( return _to_http(event=event, data_marshaller=data_marshaller) -def to_binary_http( +def to_binary( event: CloudEvent, data_marshaller: types.MarshallerType = None, ) -> (dict, typing.Union[bytes, str]): """ @@ -125,3 +145,17 @@ def to_binary_http( format=converters.TypeBinary, data_marshaller=data_marshaller, ) + + +@deprecated(deprecated_in="1.0.2", details="Use to_binary function instead") +def to_binary_http( + event: CloudEvent, data_marshaller: types.MarshallerType = None, +) -> (dict, typing.Union[bytes, str]): + return to_binary(event, data_marshaller) + + +@deprecated(deprecated_in="1.0.2", details="Use to_structured function instead") +def to_structured_http( + event: CloudEvent, data_marshaller: types.MarshallerType = None, +) -> (dict, typing.Union[bytes, str]): + return to_structured(event, data_marshaller) diff --git a/cloudevents/http/json_methods.py b/cloudevents/http/json_methods.py index 8d6bfdd..7dce14e 100644 --- a/cloudevents/http/json_methods.py +++ b/cloudevents/http/json_methods.py @@ -1,7 +1,7 @@ import typing from cloudevents.http.event import CloudEvent -from cloudevents.http.http_methods import from_http, to_structured_http +from cloudevents.http.http_methods import from_http, to_structured from cloudevents.sdk import types @@ -17,7 +17,7 @@ def to_json( :type data_marshaller: typing.Callable :returns: json object representing the given event """ - return to_structured_http(event, data_marshaller=data_marshaller)[1] + return to_structured(event, data_marshaller=data_marshaller)[1] def from_json( @@ -33,4 +33,4 @@ def from_json( :type data_unmarshaller: typing.Callable :returns: CloudEvent representing given cloudevent json object """ - return from_http(data=data, headers={}, data_unmarshaller=data_unmarshaller) + return from_http(headers={}, data=data, data_unmarshaller=data_unmarshaller) diff --git a/cloudevents/http/util.py b/cloudevents/http/util.py index d641df7..2dfb3bb 100644 --- a/cloudevents/http/util.py +++ b/cloudevents/http/util.py @@ -12,7 +12,7 @@ def default_marshaller(content: any): def _json_or_string(content: typing.Union[str, bytes]): - if len(content) == 0: + if content is None or len(content) == 0: return None try: return json.loads(content) diff --git a/cloudevents/sdk/event/base.py b/cloudevents/sdk/event/base.py index 504bba4..9903e40 100644 --- a/cloudevents/sdk/event/base.py +++ b/cloudevents/sdk/event/base.py @@ -22,7 +22,7 @@ from cloudevents.sdk import types # TODO(slinkydeveloper) is this really needed? -class EventGetterSetter(object): +class EventGetterSetter(object): # pragma: no cover # ce-specversion def CloudEventVersion(self) -> str: @@ -220,7 +220,7 @@ class BaseEvent(EventGetterSetter): missing_fields = self._ce_required_fields - raw_ce.keys() if len(missing_fields) > 0: - raise cloud_exceptions.CloudEventMissingRequiredFields( + raise cloud_exceptions.MissingRequiredFields( f"Missing required attributes: {missing_fields}" ) @@ -246,7 +246,7 @@ class BaseEvent(EventGetterSetter): missing_fields = required_binary_fields - headers.keys() if len(missing_fields) > 0: - raise cloud_exceptions.CloudEventMissingRequiredFields( + raise cloud_exceptions.MissingRequiredFields( f"Missing required attributes: {missing_fields}" ) diff --git a/cloudevents/sdk/event/v03.py b/cloudevents/sdk/event/v03.py index 03d1c1f..6070506 100644 --- a/cloudevents/sdk/event/v03.py +++ b/cloudevents/sdk/event/v03.py @@ -75,10 +75,6 @@ class Event(base.BaseEvent): def ContentEncoding(self) -> str: return self.ce__datacontentencoding.get() - @property - def datacontentencoding(self): - return self.ContentEncoding() - def SetEventType(self, eventType: str) -> base.BaseEvent: self.Set("type", eventType) return self @@ -119,6 +115,26 @@ class Event(base.BaseEvent): self.Set("datacontentencoding", contentEncoding) return self + @property + def datacontentencoding(self): + return self.ContentEncoding() + @datacontentencoding.setter def datacontentencoding(self, value: str): self.SetContentEncoding(value) + + @property + def subject(self) -> str: + return self.Subject() + + @subject.setter + def subject(self, value: str): + self.SetSubject(value) + + @property + def schema_url(self) -> str: + return self.SchemaURL() + + @schema_url.setter + def schema_url(self, value: str): + self.SetSchemaURL(value) diff --git a/cloudevents/sdk/event/v1.py b/cloudevents/sdk/event/v1.py index 782fd7a..95a6791 100644 --- a/cloudevents/sdk/event/v1.py +++ b/cloudevents/sdk/event/v1.py @@ -98,3 +98,19 @@ class Event(base.BaseEvent): def SetExtensions(self, extensions: dict) -> base.BaseEvent: self.Set("extensions", extensions) return self + + @property + def schema(self) -> str: + return self.Schema() + + @schema.setter + def schema(self, value: str): + self.SetSchema(value) + + @property + def subject(self) -> str: + return self.Subject() + + @subject.setter + def subject(self, value: str): + self.SetSubject(value) diff --git a/cloudevents/tests/test_base_events.py b/cloudevents/tests/test_base_events.py new file mode 100644 index 0000000..624734b --- /dev/null +++ b/cloudevents/tests/test_base_events.py @@ -0,0 +1,33 @@ +# 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. +import pytest + +import cloudevents.exceptions as cloud_exceptions +from cloudevents.sdk.event import v1, v03 + + +@pytest.mark.parametrize("event_class", [v1.Event, v03.Event]) +def test_unmarshall_binary_missing_fields(event_class): + event = event_class() + with pytest.raises(cloud_exceptions.MissingRequiredFields) as e: + event.UnmarshalBinary({}, "", lambda x: x) + assert "Missing required attributes: " in str(e.value) + + +@pytest.mark.parametrize("event_class", [v1.Event, v03.Event]) +def test_get_nonexistent_optional(event_class): + event = event_class() + event.SetExtensions({"ext1": "val"}) + res = event.Get("ext1") + assert res[0] == "val" and res[1] == True diff --git a/cloudevents/tests/test_converters.py b/cloudevents/tests/test_converters.py new file mode 100644 index 0000000..1e7a33d --- /dev/null +++ b/cloudevents/tests/test_converters.py @@ -0,0 +1,41 @@ +# 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. +import pytest + +from cloudevents.sdk import exceptions +from cloudevents.sdk.converters import base, binary, structured + + +def test_binary_converter_raise_unsupported(): + with pytest.raises(exceptions.UnsupportedEvent): + cnvtr = binary.BinaryHTTPCloudEventConverter() + cnvtr.read(None, {}, None, None) + + +def test_base_converters_raise_exceptions(): + with pytest.raises(Exception): + cnvtr = base.Converter() + cnvtr.event_supported(None) + + with pytest.raises(Exception): + cnvtr = base.Converter() + cnvtr.can_read(None) + + with pytest.raises(Exception): + cnvtr = base.Converter() + cnvtr.write(None, None) + + with pytest.raises(Exception): + cnvtr = base.Converter() + cnvtr.read(None, None, None, None) diff --git a/cloudevents/tests/test_data_encaps_refs.py b/cloudevents/tests/test_data_encaps_refs.py index 497334f..6ef5afc 100644 --- a/cloudevents/tests/test_data_encaps_refs.py +++ b/cloudevents/tests/test_data_encaps_refs.py @@ -92,7 +92,6 @@ def test_general_structured_properties(event_class): if key == "content-type": assert new_headers[key] == http_headers[key] continue - assert key in copy_of_ce # Test setters new_type = str(uuid4()) diff --git a/cloudevents/tests/test_deprecated_functions.py b/cloudevents/tests/test_deprecated_functions.py new file mode 100644 index 0000000..49cfffd --- /dev/null +++ b/cloudevents/tests/test_deprecated_functions.py @@ -0,0 +1,37 @@ +# 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. +import pytest + +from cloudevents.http import ( + CloudEvent, + to_binary, + to_binary_http, + to_structured, + to_structured_http, +) + + +@pytest.fixture +def event(): + return CloudEvent({"source": "s", "type": "t"}, None) + + +def test_to_binary_http_deprecated(event): + with pytest.deprecated_call(): + assert to_binary(event) == to_binary_http(event) + + +def test_to_structured_http_deprecated(event): + with pytest.deprecated_call(): + assert to_structured(event) == to_structured_http(event) diff --git a/cloudevents/tests/test_event_extensions.py b/cloudevents/tests/test_event_extensions.py index b9731ab..b2bffb2 100644 --- a/cloudevents/tests/test_event_extensions.py +++ b/cloudevents/tests/test_event_extensions.py @@ -15,12 +15,7 @@ import json import pytest -from cloudevents.http import ( - CloudEvent, - from_http, - to_binary_http, - to_structured_http, -) +from cloudevents.http import CloudEvent, from_http, to_binary, to_structured test_data = json.dumps({"data-key": "val"}) test_attributes = { @@ -39,7 +34,7 @@ def test_cloudevent_access_extensions(specversion): @pytest.mark.parametrize("specversion", ["0.3", "1.0"]) def test_to_binary_extensions(specversion): event = CloudEvent(test_attributes, test_data) - headers, body = to_binary_http(event) + headers, body = to_binary(event) assert "ce-ext1" in headers assert headers.get("ce-ext1") == test_attributes["ext1"] @@ -56,7 +51,7 @@ def test_from_binary_extensions(specversion): "ce-ext2": "test2", } body = json.dumps({"data-key": "val"}) - event = from_http(body, headers) + event = from_http(headers, body) assert headers["ce-ext1"] == event["ext1"] assert headers["ce-ext2"] == event["ext2"] @@ -65,7 +60,7 @@ def test_from_binary_extensions(specversion): @pytest.mark.parametrize("specversion", ["0.3", "1.0"]) def test_to_structured_extensions(specversion): event = CloudEvent(test_attributes, test_data) - headers, body = to_structured_http(event) + headers, body = to_structured(event) body = json.loads(body) @@ -86,7 +81,7 @@ def test_from_structured_extensions(specversion): } data = json.dumps(body) - event = from_http(data, headers) + event = from_http(headers, data) assert body["ext1"] == event["ext1"] assert body["ext2"] == event["ext2"] diff --git a/cloudevents/tests/test_event_to_request_converter.py b/cloudevents/tests/test_event_to_request_converter.py index e54264f..4bf7417 100644 --- a/cloudevents/tests/test_event_to_request_converter.py +++ b/cloudevents/tests/test_event_to_request_converter.py @@ -61,4 +61,3 @@ def test_structured_event_to_request_upstream(event_class): if key == "content-type": assert new_headers[key] == http_headers[key] continue - assert key in copy_of_ce diff --git a/cloudevents/tests/test_http_cloudevent_overrides.py b/cloudevents/tests/test_http_cloudevent.py similarity index 61% rename from cloudevents/tests/test_http_cloudevent_overrides.py rename to cloudevents/tests/test_http_cloudevent.py index 1babbe2..de9331c 100644 --- a/cloudevents/tests/test_http_cloudevent_overrides.py +++ b/cloudevents/tests/test_http_cloudevent.py @@ -1,5 +1,6 @@ import pytest +import cloudevents.exceptions as cloud_exceptions from cloudevents.http import CloudEvent @@ -69,3 +70,47 @@ def test_http_cloudevent_mutates_equality(specversion): event3.data = '{"name":"paul"}' assert event2 == event3 assert event1 != event2 and event3 != event1 + + +def test_cloudevent_missing_specversion(): + attributes = {"specversion": "0.2", "source": "s", "type": "t"} + with pytest.raises(cloud_exceptions.MissingRequiredFields) as e: + event = CloudEvent(attributes, None) + assert "Invalid specversion: 0.2" in str(e.value) + + +def test_cloudevent_missing_minimal_required_fields(): + attributes = {"type": "t"} + with pytest.raises(cloud_exceptions.MissingRequiredFields) as e: + event = CloudEvent(attributes, None) + assert f"Missing required keys: {set(['source'])}" in str(e.value) + + attributes = {"source": "s"} + with pytest.raises(cloud_exceptions.MissingRequiredFields) as e: + event = CloudEvent(attributes, None) + assert f"Missing required keys: {set(['type'])}" in str(e.value) + + +def test_cloudevent_general_overrides(): + event = CloudEvent( + { + "source": "my-source", + "type": "com.test.overrides", + "subject": "my-subject", + }, + None, + ) + expected_attributes = [ + "time", + "source", + "id", + "specversion", + "type", + "subject", + ] + + assert len(event) == 6 + for attribute in expected_attributes: + assert attribute in event + del event[attribute] + assert len(event) == 0 diff --git a/cloudevents/tests/test_http_events.py b/cloudevents/tests/test_http_events.py index b1819bf..6a9e692 100644 --- a/cloudevents/tests/test_http_events.py +++ b/cloudevents/tests/test_http_events.py @@ -25,7 +25,10 @@ from cloudevents.http import ( CloudEvent, from_http, is_binary, + is_structured, + to_binary, to_binary_http, + to_structured, to_structured_http, ) from cloudevents.sdk import converters @@ -69,17 +72,13 @@ test_data = {"payload-content": "Hello World!"} app = Sanic(__name__) -def post(url, headers, data): - return app.test_client.post(url, headers=headers, data=data) - - @app.route("/event", ["POST"]) async def echo(request): decoder = None if "binary-payload" in request.headers: decoder = lambda x: x event = from_http( - request.body, headers=dict(request.headers), data_unmarshaller=decoder + dict(request.headers), request.body, data_unmarshaller=decoder ) data = ( event.data @@ -91,25 +90,24 @@ async def echo(request): @pytest.mark.parametrize("body", invalid_cloudevent_request_body) def test_missing_required_fields_structured(body): - with pytest.raises(cloud_exceptions.CloudEventMissingRequiredFields): + with pytest.raises(cloud_exceptions.MissingRequiredFields): # CloudEvent constructor throws TypeError if missing required field # and NotImplementedError because structured calls aren't # implemented. In this instance one of the required keys should have # prefix e-id instead of ce-id therefore it should throw _ = from_http( - json.dumps(body), - headers={"Content-Type": "application/cloudevents+json"}, + {"Content-Type": "application/cloudevents+json"}, json.dumps(body), ) @pytest.mark.parametrize("headers", invalid_test_headers) def test_missing_required_fields_binary(headers): - with pytest.raises(cloud_exceptions.CloudEventMissingRequiredFields): + with pytest.raises(cloud_exceptions.MissingRequiredFields): # CloudEvent constructor throws TypeError if missing required field # and NotImplementedError because structured calls aren't # implemented. In this instance one of the required keys should have # prefix e-id instead of ce-id therefore it should throw - _ = from_http(json.dumps(test_data), headers=headers) + _ = from_http(headers, json.dumps(test_data)) @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) @@ -177,9 +175,9 @@ def test_roundtrip_non_json_event(converter, specversion): event = CloudEvent(attrs, compressed_data) if converter == converters.TypeStructured: - headers, data = to_structured_http(event, data_marshaller=lambda x: x) + headers, data = to_structured(event, data_marshaller=lambda x: x) elif converter == converters.TypeBinary: - headers, data = to_binary_http(event, data_marshaller=lambda x: x) + headers, data = to_binary(event, data_marshaller=lambda x: x) headers["binary-payload"] = "true" # Decoding hint for server _, r = app.test_client.post("/event", headers=headers, data=data) @@ -204,12 +202,12 @@ def test_missing_ce_prefix_binary_event(specversion): # breaking prefix e.g. e-id instead of ce-id prefixed_headers[key[1:]] = headers[key] - with pytest.raises(cloud_exceptions.CloudEventMissingRequiredFields): + with pytest.raises(cloud_exceptions.MissingRequiredFields): # CloudEvent constructor throws TypeError if missing required field # and NotImplementedError because structured calls aren't # implemented. In this instance one of the required keys should have # prefix e-id instead of ce-id therefore it should throw - _ = from_http(json.dumps(test_data), headers=prefixed_headers) + _ = from_http(prefixed_headers, json.dumps(test_data)) @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) @@ -226,7 +224,7 @@ def test_valid_binary_events(specversion): "ce-specversion": specversion, } data = {"payload": f"payload-{i}"} - events_queue.append(from_http(json.dumps(data), headers=headers)) + events_queue.append(from_http(headers, json.dumps(data))) for i, event in enumerate(events_queue): data = event.data @@ -247,7 +245,7 @@ def test_structured_to_request(specversion): data = {"message": "Hello World!"} event = CloudEvent(attributes, data) - headers, body_bytes = to_structured_http(event) + headers, body_bytes = to_structured(event) assert isinstance(body_bytes, bytes) body = json.loads(body_bytes) @@ -267,7 +265,7 @@ def test_binary_to_request(specversion): } data = {"message": "Hello World!"} event = CloudEvent(attributes, data) - headers, body_bytes = to_binary_http(event) + headers, body_bytes = to_binary(event) body = json.loads(body_bytes) for key in data: @@ -289,7 +287,7 @@ def test_empty_data_structured_event(specversion): } _ = from_http( - json.dumps(attributes), {"content-type": "application/cloudevents+json"} + {"content-type": "application/cloudevents+json"}, json.dumps(attributes) ) @@ -304,7 +302,7 @@ def test_empty_data_binary_event(specversion): "ce-time": "2018-10-23T12:28:22.4579346Z", "ce-source": "", } - _ = from_http("", headers) + _ = from_http(headers, "") @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) @@ -322,8 +320,8 @@ def test_valid_structured_events(specversion): } events_queue.append( from_http( - json.dumps(event), {"content-type": "application/cloudevents+json"}, + json.dumps(event), ) ) @@ -344,7 +342,7 @@ def test_structured_no_content_type(specversion): "specversion": specversion, "data": test_data, } - event = from_http(json.dumps(data), {},) + event = from_http({}, json.dumps(data)) assert event["id"] == "id" assert event["source"] == "source.com.test" @@ -382,7 +380,7 @@ def test_cloudevent_repr(specversion): "ce-time": "2018-10-23T12:28:22.4579346Z", "ce-source": "", } - event = from_http("", headers) + event = from_http(headers, "") # Testing to make sure event is printable. I could runevent. __repr__() but # we had issues in the past where event.__repr__() could run but # print(event) would fail. @@ -398,5 +396,79 @@ def test_none_data_cloudevent(specversion): "specversion": specversion, } ) - to_binary_http(event) - to_structured_http(event) + to_binary(event) + to_structured(event) + + +def test_wrong_specversion(): + headers = {"Content-Type": "application/cloudevents+json"} + data = json.dumps( + { + "specversion": "0.2", + "type": "word.found.name", + "id": "96fb5f0b-001e-0108-6dfe-da6e2806f124", + "source": "", + } + ) + with pytest.raises(cloud_exceptions.InvalidRequiredFields) as e: + from_http(headers, data) + assert "Found invalid specversion 0.2" in str(e.value) + + +def test_invalid_data_format_structured_from_http(): + headers = {"Content-Type": "application/cloudevents+json"} + data = 20 + with pytest.raises(cloud_exceptions.InvalidStructuredJSON) as e: + from_http(headers, data) + assert "Expected json of type (str, bytes, bytearray)" in str(e.value) + + +def test_wrong_specversion_to_request(): + event = CloudEvent({"source": "s", "type": "t"}, None) + with pytest.raises(cloud_exceptions.InvalidRequiredFields) as e: + event["specversion"] = "0.2" + to_binary(event) + assert "Unsupported specversion: 0.2" in str(e.value) + + +def test_is_structured(): + headers = { + "Content-Type": "application/cloudevents+json", + } + assert is_structured(headers) + + headers = { + "ce-id": "my-id", + "ce-source": "", + "ce-type": "cloudevent.event.type", + "ce-specversion": "1.0", + "Content-Type": "text/plain", + } + assert not is_structured(headers) + + +def test_empty_json_structured(): + headers = {"Content-Type": "application/cloudevents+json"} + data = "" + with pytest.raises(cloud_exceptions.InvalidStructuredJSON) as e: + from_http( + headers, data, + ) + assert "Failed to read fields from structured event. " in str(e.value) + + +def test_uppercase_headers_with_none_data_binary(): + headers = { + "Ce-Id": "my-id", + "Ce-Source": "", + "Ce-Type": "cloudevent.event.type", + "Ce-Specversion": "1.0", + } + event = from_http(headers, None) + + for key in headers: + assert event[key.lower()[3:]] == headers[key] + assert event.data == None + + _, new_data = to_binary(event) + assert new_data == None diff --git a/cloudevents/tests/test_marshaller.py b/cloudevents/tests/test_marshaller.py new file mode 100644 index 0000000..2bb0e37 --- /dev/null +++ b/cloudevents/tests/test_marshaller.py @@ -0,0 +1,63 @@ +# 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. + +import pytest + +from cloudevents.sdk import converters, exceptions, marshaller +from cloudevents.sdk.converters import binary, structured +from cloudevents.sdk.event import v1 + + +@pytest.fixture +def headers(): + return { + "ce-specversion": "1.0", + "ce-source": "1.0", + "ce-type": "com.marshaller.test", + "ce-id": "1234-1234-1234", + } + + +def test_from_request_wrong_unmarshaller(): + with pytest.raises(exceptions.InvalidDataUnmarshaller): + m = marshaller.NewDefaultHTTPMarshaller() + _ = m.FromRequest(v1.Event(), {}, "", None) + + +def test_to_request_wrong_marshaller(): + with pytest.raises(exceptions.InvalidDataMarshaller): + m = marshaller.NewDefaultHTTPMarshaller() + _ = m.ToRequest(v1.Event(), data_marshaller="") + + +def test_from_request_cannot_read(headers): + with pytest.raises(exceptions.UnsupportedEventConverter): + m = marshaller.HTTPMarshaller( + [binary.NewBinaryHTTPCloudEventConverter(),] + ) + m.FromRequest(v1.Event(), {}, "") + + with pytest.raises(exceptions.UnsupportedEventConverter): + m = marshaller.HTTPMarshaller( + [structured.NewJSONHTTPCloudEventConverter()] + ) + m.FromRequest(v1.Event(), headers, "") + + +def test_to_request_invalid_converter(): + with pytest.raises(exceptions.NoSuchConverter): + m = marshaller.HTTPMarshaller( + [structured.NewJSONHTTPCloudEventConverter()] + ) + m.ToRequest(v1.Event(), "") diff --git a/cloudevents/tests/test_options.py b/cloudevents/tests/test_options.py new file mode 100644 index 0000000..1992862 --- /dev/null +++ b/cloudevents/tests/test_options.py @@ -0,0 +1,36 @@ +# 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. + +import pytest + +from cloudevents.sdk.event.opt import Option + + +def test_set_raise_error(): + with pytest.raises(ValueError): + o = Option("test", "value", True) + o.set(None) + + +def test_options_eq_override(): + o = Option("test", "value", True) + assert o.required() + + o2 = Option("test", "value", True) + assert o2.required() + + assert o == o2 + o.set("setting to new value") + + assert o != o2 diff --git a/cloudevents/tests/test_v03_event.py b/cloudevents/tests/test_v03_event.py new file mode 100644 index 0000000..b7a7e7e --- /dev/null +++ b/cloudevents/tests/test_v03_event.py @@ -0,0 +1,64 @@ +# 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. +import pytest + +from cloudevents.sdk.event import v03 + + +def test_v03_time_property(): + event = v03.Event() + + time1 = "1234" + event.time = time1 + assert event.EventTime() == time1 + + time2 = "4321" + event.SetEventTime(time2) + assert event.time == time2 + + +def test_v03_subject_property(): + event = v03.Event() + + subject1 = "" + event.subject = subject1 + assert event.Subject() == subject1 + + subject2 = "" + event.SetSubject(subject2) + assert event.subject == subject2 + + +def test_v03_schema_url_property(): + event = v03.Event() + + schema_url1 = "" + event.schema_url = schema_url1 + assert event.SchemaURL() == schema_url1 + + schema_url2 = "" + event.SetSchemaURL(schema_url2) + assert event.schema_url == schema_url2 + + +def test_v03_datacontentencoding_property(): + event = v03.Event() + + datacontentencoding1 = "" + event.datacontentencoding = datacontentencoding1 + assert event.ContentEncoding() == datacontentencoding1 + + datacontentencoding2 = "" + event.SetContentEncoding(datacontentencoding2) + assert event.datacontentencoding == datacontentencoding2 diff --git a/cloudevents/tests/test_v1_event.py b/cloudevents/tests/test_v1_event.py new file mode 100644 index 0000000..70905e9 --- /dev/null +++ b/cloudevents/tests/test_v1_event.py @@ -0,0 +1,53 @@ +# 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. + +import pytest + +from cloudevents.sdk.event import v1 + + +def test_v1_time_property(): + event = v1.Event() + + time1 = "1234" + event.time = time1 + assert event.EventTime() == time1 + + time2 = "4321" + event.SetEventTime(time2) + assert event.time == time2 + + +def test_v1_subject_property(): + event = v1.Event() + + subject1 = "" + event.subject = subject1 + assert event.Subject() == subject1 + + subject2 = "" + event.SetSubject(subject2) + assert event.subject == subject2 + + +def test_v1_schema_property(): + event = v1.Event() + + schema1 = "" + event.schema = schema1 + assert event.Schema() == schema1 + + schema2 = "" + event.SetSchema(schema2) + assert event.schema == schema2 diff --git a/samples/http-image-cloudevents/client.py b/samples/http-image-cloudevents/client.py index 3b856d1..3714fca 100644 --- a/samples/http-image-cloudevents/client.py +++ b/samples/http-image-cloudevents/client.py @@ -15,7 +15,7 @@ import sys import requests -from cloudevents.http import CloudEvent, to_binary_http, to_structured_http +from cloudevents.http import CloudEvent, to_binary, to_structured resp = requests.get( "https://raw.githubusercontent.com/cncf/artwork/master/projects/cloudevents/horizontal/color/cloudevents-horizontal-color.png" @@ -33,7 +33,7 @@ def send_binary_cloud_event(url: str): event = CloudEvent(attributes, image_bytes) # Create cloudevent HTTP headers and content - headers, body = to_binary_http(event) + headers, body = to_binary(event) # Send cloudevent requests.post(url, headers=headers, data=body) @@ -50,10 +50,10 @@ def send_structured_cloud_event(url: str): event = CloudEvent(attributes, image_bytes) # Create cloudevent HTTP headers and content - # Note that to_structured_http will create a data_base64 data field in + # Note that to_structured will create a data_base64 data field in # specversion 1.0 (default specversion) if given # an event whose data field is of type bytes. - headers, body = to_structured_http(event) + headers, body = to_structured(event) # Send cloudevent requests.post(url, headers=headers, data=body) diff --git a/samples/http-image-cloudevents/image_sample_server.py b/samples/http-image-cloudevents/image_sample_server.py index 07d9a89..20f7dfc 100644 --- a/samples/http-image-cloudevents/image_sample_server.py +++ b/samples/http-image-cloudevents/image_sample_server.py @@ -26,8 +26,8 @@ def home(): # Create a CloudEvent. # data_unmarshaller will cast event.data into an io.BytesIO object event = from_http( - request.get_data(), request.headers, + request.get_data(), data_unmarshaller=lambda x: io.BytesIO(x), ) diff --git a/samples/http-image-cloudevents/image_sample_test.py b/samples/http-image-cloudevents/image_sample_test.py index 64c0be2..2ca47b9 100644 --- a/samples/http-image-cloudevents/image_sample_test.py +++ b/samples/http-image-cloudevents/image_sample_test.py @@ -7,12 +7,7 @@ from client import image_bytes from image_sample_server import app from PIL import Image -from cloudevents.http import ( - CloudEvent, - from_http, - to_binary_http, - to_structured_http, -) +from cloudevents.http import CloudEvent, from_http, to_binary, to_structured image_fileobj = io.BytesIO(image_bytes) image_expected_shape = (1880, 363) @@ -35,11 +30,11 @@ def test_create_binary_image(): event = CloudEvent(attributes, image_bytes) # Create http headers/body content - headers, body = to_binary_http(event) + headers, body = to_binary(event) # Unmarshall CloudEvent and re-create image reconstruct_event = from_http( - body, headers, data_unmarshaller=lambda x: io.BytesIO(x) + headers, body, data_unmarshaller=lambda x: io.BytesIO(x) ) # reconstruct_event.data is an io.BytesIO object due to data_unmarshaller @@ -62,7 +57,7 @@ def test_create_structured_image(): event = CloudEvent(attributes, image_bytes) # Create http headers/body content - headers, body = to_structured_http(event) + headers, body = to_structured(event) # Structured has cloudevent attributes marshalled inside the body. For this # reason we must load the byte object to create the python dict containing @@ -75,7 +70,7 @@ def test_create_structured_image(): # Unmarshall CloudEvent and re-create image reconstruct_event = from_http( - body, headers, data_unmarshaller=lambda x: io.BytesIO(x) + headers, body, data_unmarshaller=lambda x: io.BytesIO(x) ) # reconstruct_event.data is an io.BytesIO object due to data_unmarshaller @@ -92,10 +87,10 @@ def test_server_structured(client): event = CloudEvent(attributes, image_bytes) # Create cloudevent HTTP headers and content - # Note that to_structured_http will create a data_base64 data field in + # Note that to_structured will create a data_base64 data field in # specversion 1.0 (default specversion) if given # an event whose data field is of type bytes. - headers, body = to_structured_http(event) + headers, body = to_structured(event) # Send cloudevent r = client.post("/", headers=headers, data=body) @@ -113,7 +108,7 @@ def test_server_binary(client): event = CloudEvent(attributes, image_bytes) # Create cloudevent HTTP headers and content - headers, body = to_binary_http(event) + headers, body = to_binary(event) # Send cloudevent r = client.post("/", headers=headers, data=body) diff --git a/samples/http-json-cloudevents/client.py b/samples/http-json-cloudevents/client.py index a77fd33..eff6f4d 100644 --- a/samples/http-json-cloudevents/client.py +++ b/samples/http-json-cloudevents/client.py @@ -16,7 +16,7 @@ import sys import requests -from cloudevents.http import CloudEvent, to_binary_http, to_structured_http +from cloudevents.http import CloudEvent, to_binary, to_structured def send_binary_cloud_event(url): @@ -28,7 +28,7 @@ def send_binary_cloud_event(url): data = {"message": "Hello World!"} event = CloudEvent(attributes, data) - headers, body = to_binary_http(event) + headers, body = to_binary(event) # send and print event requests.post(url, headers=headers, data=body) @@ -44,7 +44,7 @@ def send_structured_cloud_event(url): data = {"message": "Hello World!"} event = CloudEvent(attributes, data) - headers, body = to_structured_http(event) + headers, body = to_structured(event) # send and print event requests.post(url, headers=headers, data=body) diff --git a/samples/http-json-cloudevents/json_sample_server.py b/samples/http-json-cloudevents/json_sample_server.py index c36afc8..920324d 100644 --- a/samples/http-json-cloudevents/json_sample_server.py +++ b/samples/http-json-cloudevents/json_sample_server.py @@ -22,7 +22,7 @@ app = Flask(__name__) @app.route("/", methods=["POST"]) def home(): # create a CloudEvent - event = from_http(request.get_data(), request.headers) + event = from_http(request.headers, request.get_data()) # you can access cloudevent fields as seen below print( diff --git a/samples/http-json-cloudevents/json_sample_test.py b/samples/http-json-cloudevents/json_sample_test.py index 4ab9708..94f88e1 100644 --- a/samples/http-json-cloudevents/json_sample_test.py +++ b/samples/http-json-cloudevents/json_sample_test.py @@ -1,7 +1,7 @@ import pytest from json_sample_server import app -from cloudevents.http import CloudEvent, to_binary_http, to_structured_http +from cloudevents.http import CloudEvent, to_binary, to_structured @pytest.fixture @@ -19,7 +19,7 @@ def test_binary_request(client): data = {"message": "Hello World!"} event = CloudEvent(attributes, data) - headers, body = to_binary_http(event) + headers, body = to_binary(event) r = client.post("/", headers=headers, data=body) assert r.status_code == 204 @@ -34,7 +34,7 @@ def test_structured_request(client): data = {"message": "Hello World!"} event = CloudEvent(attributes, data) - headers, body = to_structured_http(event) + headers, body = to_structured(event) r = client.post("/", headers=headers, data=body) assert r.status_code == 204 diff --git a/setup.py b/setup.py index 053ea6c..dea6015 100644 --- a/setup.py +++ b/setup.py @@ -41,4 +41,5 @@ setup( ], packages=find_packages(exclude=["cloudevents.tests"]), version=pypi_config["version_target"], + install_requires=["deprecation>=2.0,<3.0"], ) diff --git a/tox.ini b/tox.ini index 49745e4..0d54b17 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ deps = -r{toxinidir}/requirements/docs.txt -r{toxinidir}/requirements/publish.txt setenv = - PYTESTARGS = -v -s --tb=long --cov=cloudevents + PYTESTARGS = -v -s --tb=long --cov=cloudevents --cov-report term-missing --cov-fail-under=100 commands = pytest {env:PYTESTARGS} {posargs} [testenv:reformat]