diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 00000000..22880d42 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,4 @@ +[settings] +line_length = 80 +multi_line_output = 3 +include_trailing_comma = True diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..ed9f8e11 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: +- repo: https://github.com/timothycrosley/isort/ + rev: 5.0.4 + hooks: + - id: isort +- repo: https://github.com/psf/black + rev: 19.10b0 + hooks: + - id: black + language_version: python3.8 diff --git a/README.md b/README.md index a52bdcb2..f911f0d2 100644 --- a/README.md +++ b/README.md @@ -217,3 +217,14 @@ the same API. It will use semantic versioning with following rules: [CNCF's Slack workspace](https://slack.cncf.io/). - Email: https://lists.cncf.io/g/cncf-cloudevents-sdk - Contact for additional information: Denis Makogon (`@denysmakogon` on slack). + +## Maintenance + +We use black and isort for autoformatting. We setup a tox environment to reformat +the codebase. + +e.g. +```python +pip install tox +tox -e reformat +``` diff --git a/cloudevents/sdk/converters/__init__.py b/cloudevents/sdk/converters/__init__.py index ee2fc412..1e786455 100644 --- a/cloudevents/sdk/converters/__init__.py +++ b/cloudevents/sdk/converters/__init__.py @@ -12,8 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudevents.sdk.converters import binary -from cloudevents.sdk.converters import structured +from cloudevents.sdk.converters import binary, structured TypeBinary = binary.BinaryHTTPCloudEventConverter.TYPE TypeStructured = structured.JSONHTTPCloudEventConverter.TYPE diff --git a/cloudevents/sdk/converters/base.py b/cloudevents/sdk/converters/base.py index 69bf8cb0..aa75f7c7 100644 --- a/cloudevents/sdk/converters/base.py +++ b/cloudevents/sdk/converters/base.py @@ -26,7 +26,7 @@ def read( event, headers: dict, body: typing.IO, - data_unmarshaller: typing.Callable + data_unmarshaller: typing.Callable, ) -> base.BaseEvent: raise Exception("not implemented") @@ -37,8 +37,6 @@ def can_read(self, content_type: str) -> bool: raise Exception("not implemented") def write( - self, - event: base.BaseEvent, - data_marshaller: typing.Callable + self, event: base.BaseEvent, data_marshaller: typing.Callable ) -> (dict, object): raise Exception("not implemented") diff --git a/cloudevents/sdk/converters/binary.py b/cloudevents/sdk/converters/binary.py index dfb08fe6..abdc9d99 100644 --- a/cloudevents/sdk/converters/binary.py +++ b/cloudevents/sdk/converters/binary.py @@ -17,7 +17,7 @@ from cloudevents.sdk import exceptions, types from cloudevents.sdk.converters import base from cloudevents.sdk.event import base as event_base -from cloudevents.sdk.event import v03, v1 +from cloudevents.sdk.event import v1, v03 class BinaryHTTPCloudEventConverter(base.Converter): @@ -44,9 +44,7 @@ def read( return event def write( - self, - event: event_base.BaseEvent, - data_marshaller: types.MarshallerType + self, event: event_base.BaseEvent, data_marshaller: types.MarshallerType ) -> (dict, bytes): return event.MarshalBinary(data_marshaller) diff --git a/cloudevents/sdk/converters/structured.py b/cloudevents/sdk/converters/structured.py index 435e004f..d29a9284 100644 --- a/cloudevents/sdk/converters/structured.py +++ b/cloudevents/sdk/converters/structured.py @@ -15,7 +15,6 @@ import typing from cloudevents.sdk import types - from cloudevents.sdk.converters import base from cloudevents.sdk.event import base as event_base @@ -43,9 +42,7 @@ def read( return event def write( - self, - event: event_base.BaseEvent, - data_marshaller: types.MarshallerType + self, event: event_base.BaseEvent, data_marshaller: types.MarshallerType ) -> (dict, bytes): http_headers = {"content-type": self.MIME_TYPE} return http_headers, event.MarshalJSON(data_marshaller).encode("utf-8") diff --git a/cloudevents/sdk/event/base.py b/cloudevents/sdk/event/base.py index 25ba6bd7..791eb679 100644 --- a/cloudevents/sdk/event/base.py +++ b/cloudevents/sdk/event/base.py @@ -206,7 +206,7 @@ def MarshalJSON(self, data_marshaller: types.MarshallerType) -> str: def UnmarshalJSON( self, b: typing.Union[str, bytes], - data_unmarshaller: types.UnmarshallerType + data_unmarshaller: types.UnmarshallerType, ): raw_ce = json.loads(b) @@ -228,9 +228,9 @@ def UnmarshalBinary( self, headers: dict, body: typing.Union[bytes, str], - data_unmarshaller: types.UnmarshallerType + data_unmarshaller: types.UnmarshallerType, ): - if 'ce-specversion' not in headers: + if "ce-specversion" not in headers: raise ValueError("Missing required attribute: 'specversion'") for header, value in headers.items(): header = header.lower() @@ -244,8 +244,7 @@ def UnmarshalBinary( raise ValueError(f"Missing required attributes: {missing_attrs}") def MarshalBinary( - self, - data_marshaller: types.MarshallerType + self, data_marshaller: types.MarshallerType ) -> (dict, bytes): if data_marshaller is None: data_marshaller = json.dumps diff --git a/cloudevents/sdk/event/opt.py b/cloudevents/sdk/event/opt.py index bf630c32..e28d84f3 100644 --- a/cloudevents/sdk/event/opt.py +++ b/cloudevents/sdk/event/opt.py @@ -24,8 +24,8 @@ def set(self, new_value): if self.is_required and is_none: raise ValueError( "Attribute value error: '{0}', " - "" "invalid new value." - .format(self.name) + "" + "invalid new value.".format(self.name) ) self.value = new_value @@ -37,7 +37,9 @@ def required(self): return self.is_required def __eq__(self, obj): - return isinstance(obj, Option) and \ - obj.name == self.name and \ - obj.value == self.value and \ - obj.is_required == self.is_required + return ( + isinstance(obj, Option) + and obj.name == self.name + and obj.value == self.value + and obj.is_required == self.is_required + ) diff --git a/cloudevents/sdk/event/v03.py b/cloudevents/sdk/event/v03.py index 3b6a3222..03d1c1f4 100644 --- a/cloudevents/sdk/event/v03.py +++ b/cloudevents/sdk/event/v03.py @@ -12,24 +12,18 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudevents.sdk.event import base -from cloudevents.sdk.event import opt +from cloudevents.sdk.event import base, opt class Event(base.BaseEvent): - _ce_required_fields = { - 'id', - 'source', - 'type', - 'specversion' - } + _ce_required_fields = {"id", "source", "type", "specversion"} _ce_optional_fields = { - 'datacontentencoding', - 'datacontenttype', - 'schemaurl', - 'subject', - 'time' + "datacontentencoding", + "datacontenttype", + "schemaurl", + "subject", + "time", } def __init__(self): @@ -40,9 +34,7 @@ def __init__(self): self.ce__datacontenttype = opt.Option("datacontenttype", None, False) self.ce__datacontentencoding = opt.Option( - "datacontentencoding", - None, - False + "datacontentencoding", None, False ) self.ce__subject = opt.Option("subject", None, False) self.ce__time = opt.Option("time", None, False) diff --git a/cloudevents/sdk/event/v1.py b/cloudevents/sdk/event/v1.py index 6034a9b4..782fd7ac 100644 --- a/cloudevents/sdk/event/v1.py +++ b/cloudevents/sdk/event/v1.py @@ -12,24 +12,13 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudevents.sdk.event import base -from cloudevents.sdk.event import opt +from cloudevents.sdk.event import base, opt class Event(base.BaseEvent): - _ce_required_fields = { - 'id', - 'source', - 'type', - 'specversion' - } - - _ce_optional_fields = { - 'datacontenttype', - 'dataschema', - 'subject', - 'time' - } + _ce_required_fields = {"id", "source", "type", "specversion"} + + _ce_optional_fields = {"datacontenttype", "dataschema", "subject", "time"} def __init__(self): self.ce__specversion = opt.Option("specversion", "1.0", True) diff --git a/cloudevents/sdk/exceptions.py b/cloudevents/sdk/exceptions.py index 2f30db04..3195f90e 100644 --- a/cloudevents/sdk/exceptions.py +++ b/cloudevents/sdk/exceptions.py @@ -15,9 +15,7 @@ class UnsupportedEvent(Exception): def __init__(self, event_class): - super().__init__( - "Invalid CloudEvent class: '{0}'".format(event_class) - ) + super().__init__("Invalid CloudEvent class: '{0}'".format(event_class)) class InvalidDataUnmarshaller(Exception): @@ -27,16 +25,12 @@ def __init__(self): class InvalidDataMarshaller(Exception): def __init__(self): - super().__init__( - "Invalid data marshaller, is not a callable" - ) + super().__init__("Invalid data marshaller, is not a callable") class NoSuchConverter(Exception): def __init__(self, converter_type): - super().__init__( - "No such converter {0}".format(converter_type) - ) + super().__init__("No such converter {0}".format(converter_type)) class UnsupportedEventConverter(Exception): diff --git a/cloudevents/sdk/http_events.py b/cloudevents/sdk/http_events.py index b27c1a26..9ca3abc6 100644 --- a/cloudevents/sdk/http_events.py +++ b/cloudevents/sdk/http_events.py @@ -17,11 +17,8 @@ import typing import uuid -from cloudevents.sdk import converters -from cloudevents.sdk import marshaller -from cloudevents.sdk import types - -from cloudevents.sdk.event import v03, v1 +from cloudevents.sdk import converters, marshaller, types +from cloudevents.sdk.event import v1, v03 _marshaller_by_format = { converters.TypeStructured: lambda x: x, @@ -43,7 +40,7 @@ def _json_or_string(content: typing.Union[str, bytes]): return content -class CloudEvent(): +class CloudEvent: """ Python-friendly cloudevent class supporting v1 events Supports both binary and structured mode CloudEvents @@ -54,7 +51,7 @@ def from_http( cls, data: typing.Union[str, bytes], headers: typing.Dict[str, str], - data_unmarshaller: types.UnmarshallerType = None + data_unmarshaller: types.UnmarshallerType = None, ): """Unwrap a CloudEvent (binary or structured) from an HTTP request. :param data: the HTTP request body @@ -68,18 +65,17 @@ def from_http( data_unmarshaller = _json_or_string event = marshaller.NewDefaultHTTPMarshaller().FromRequest( - v1.Event(), headers, data, data_unmarshaller) + v1.Event(), headers, data, data_unmarshaller + ) attrs = event.Properties() - attrs.pop('data', None) - attrs.pop('extensions', None) + attrs.pop("data", None) + attrs.pop("extensions", None) attrs.update(**event.extensions) return cls(attrs, event.data) def __init__( - self, - attributes: typing.Dict[str, str], - data: typing.Any = None + self, attributes: typing.Dict[str, str], data: typing.Any = None ): """ Event Constructor @@ -97,28 +93,31 @@ def __init__( """ self._attributes = {k.lower(): v for k, v in attributes.items()} self.data = data - if 'specversion' not in self._attributes: - self._attributes['specversion'] = "1.0" - if 'id' not in self._attributes: - self._attributes['id'] = str(uuid.uuid4()) - if 'time' not in self._attributes: - self._attributes['time'] = datetime.datetime.now( - datetime.timezone.utc).isoformat() - - if self._attributes['specversion'] not in _required_by_version: + if "specversion" not in self._attributes: + self._attributes["specversion"] = "1.0" + if "id" not in self._attributes: + self._attributes["id"] = str(uuid.uuid4()) + if "time" not in self._attributes: + self._attributes["time"] = datetime.datetime.now( + datetime.timezone.utc + ).isoformat() + + if self._attributes["specversion"] not in _required_by_version: raise ValueError( - f"Invalid specversion: {self._attributes['specversion']}") + 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']] + required_set = _required_by_version[self._attributes["specversion"]] if not required_set <= self._attributes.keys(): raise ValueError( - f"Missing required keys: {required_set - attributes.keys()}") + f"Missing required keys: {required_set - attributes.keys()}" + ) def to_http( self, format: str = converters.TypeStructured, - data_marshaller: types.MarshallerType = None + data_marshaller: types.MarshallerType = None, ) -> (dict, typing.Union[bytes, str]): """ Returns a tuple of HTTP headers/body dicts representing this cloudevent @@ -133,7 +132,8 @@ def to_http( data_marshaller = _marshaller_by_format[format] if self._attributes["specversion"] not in _obj_by_version: raise ValueError( - f"Unsupported specversion: {self._attributes['specversion']}") + f"Unsupported specversion: {self._attributes['specversion']}" + ) event = _obj_by_version[self._attributes["specversion"]]() for k, v in self._attributes.items(): @@ -141,7 +141,8 @@ def to_http( event.data = self.data return marshaller.NewDefaultHTTPMarshaller().ToRequest( - event, format, data_marshaller) + event, format, data_marshaller + ) # Data access is handled via `.data` member # Attribute access is managed via Mapping type diff --git a/cloudevents/sdk/marshaller.py b/cloudevents/sdk/marshaller.py index 1276dca3..d9fe4920 100644 --- a/cloudevents/sdk/marshaller.py +++ b/cloudevents/sdk/marshaller.py @@ -16,11 +16,7 @@ import typing from cloudevents.sdk import exceptions, types - -from cloudevents.sdk.converters import base -from cloudevents.sdk.converters import binary -from cloudevents.sdk.converters import structured - +from cloudevents.sdk.converters import base, binary, structured from cloudevents.sdk.event import base as event_base @@ -93,8 +89,9 @@ def ToRequest( :return: dict of HTTP headers and stream of HTTP request body :rtype: tuple """ - if (data_marshaller is not None - and not isinstance(data_marshaller, typing.Callable)): + if data_marshaller is not None and not isinstance( + data_marshaller, typing.Callable + ): raise exceptions.InvalidDataMarshaller() if converter_type is None: @@ -123,7 +120,7 @@ def NewDefaultHTTPMarshaller() -> HTTPMarshaller: def NewHTTPMarshaller( - converters: typing.List[base.Converter] + converters: typing.List[base.Converter], ) -> HTTPMarshaller: """ Creates the default HTTP marshaller with both diff --git a/cloudevents/sdk/types.py b/cloudevents/sdk/types.py index 82885842..1a302ea2 100644 --- a/cloudevents/sdk/types.py +++ b/cloudevents/sdk/types.py @@ -18,7 +18,8 @@ # both JSON and Binary format. MarshallerType = typing.Optional[ - typing.Callable[[typing.Any], typing.Union[bytes, str]]] + typing.Callable[[typing.Any], typing.Union[bytes, str]] +] UnmarshallerType = typing.Optional[ - typing.Callable[ - [typing.Union[bytes, str]], typing.Any]] + typing.Callable[[typing.Union[bytes, str]], typing.Any] +] diff --git a/cloudevents/tests/data.py b/cloudevents/tests/data.py index ffe63aee..353aac50 100644 --- a/cloudevents/tests/data.py +++ b/cloudevents/tests/data.py @@ -12,7 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudevents.sdk.event import v03, v1 +from cloudevents.sdk.event import v1, v03 contentType = "application/json" ce_type = "word.found.exclamation" diff --git a/cloudevents/tests/test_data_encaps_refs.py b/cloudevents/tests/test_data_encaps_refs.py index 3afb40e8..497334f3 100644 --- a/cloudevents/tests/test_data_encaps_refs.py +++ b/cloudevents/tests/test_data_encaps_refs.py @@ -12,20 +12,16 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import io import json -import copy -import pytest - from uuid import uuid4 -from cloudevents.sdk import converters -from cloudevents.sdk import marshaller +import pytest +from cloudevents.sdk import converters, marshaller from cloudevents.sdk.converters import structured -from cloudevents.sdk.event import v03, v1 - - +from cloudevents.sdk.event import v1, v03 from cloudevents.tests import data @@ -56,7 +52,7 @@ def test_general_binary_properties(event_class): new_content_type = str(uuid4()) new_source = str(uuid4()) - event.extensions = {'test': str(uuid4)} + event.extensions = {"test": str(uuid4)} event.type = new_type event.id = new_id event.content_type = new_content_type @@ -66,10 +62,11 @@ def test_general_binary_properties(event_class): assert (event.type == new_type) and (event.type == event.EventType()) assert (event.id == new_id) and (event.id == event.EventID()) assert (event.content_type == new_content_type) and ( - event.content_type == event.ContentType()) + event.content_type == event.ContentType() + ) assert (event.source == new_source) and (event.source == event.Source()) - assert event.extensions['test'] == event.Extensions()['test'] - assert (event.specversion == event.CloudEventVersion()) + assert event.extensions["test"] == event.Extensions()["test"] + assert event.specversion == event.CloudEventVersion() @pytest.mark.parametrize("event_class", [v03.Event, v1.Event]) @@ -78,8 +75,10 @@ def test_general_structured_properties(event_class): m = marshaller.NewDefaultHTTPMarshaller() http_headers = {"content-type": "application/cloudevents+json"} event = m.FromRequest( - event_class(), http_headers, json.dumps( - data.json_ce[event_class]), lambda x: x + event_class(), + http_headers, + json.dumps(data.json_ce[event_class]), + lambda x: x, ) # Test python properties assert event is not None @@ -101,7 +100,7 @@ def test_general_structured_properties(event_class): new_content_type = str(uuid4()) new_source = str(uuid4()) - event.extensions = {'test': str(uuid4)} + event.extensions = {"test": str(uuid4)} event.type = new_type event.id = new_id event.content_type = new_content_type @@ -111,7 +110,8 @@ def test_general_structured_properties(event_class): assert (event.type == new_type) and (event.type == event.EventType()) assert (event.id == new_id) and (event.id == event.EventID()) assert (event.content_type == new_content_type) and ( - event.content_type == event.ContentType()) + event.content_type == event.ContentType() + ) assert (event.source == new_source) and (event.source == event.Source()) - assert event.extensions['test'] == event.Extensions()['test'] - assert (event.specversion == event.CloudEventVersion()) + assert event.extensions["test"] == event.Extensions()["test"] + assert event.specversion == event.CloudEventVersion() diff --git a/cloudevents/tests/test_event_from_request_converter.py b/cloudevents/tests/test_event_from_request_converter.py index 7163533c..b291b01e 100644 --- a/cloudevents/tests/test_event_from_request_converter.py +++ b/cloudevents/tests/test_event_from_request_converter.py @@ -12,31 +12,24 @@ # License for the specific language governing permissions and limitations # under the License. -import json -import pytest import io +import json -from cloudevents.sdk import exceptions -from cloudevents.sdk import marshaller - -from cloudevents.sdk.event import v03 -from cloudevents.sdk.event import v1 - -from cloudevents.sdk.converters import binary -from cloudevents.sdk.converters import structured +import pytest +from cloudevents.sdk import exceptions, marshaller +from cloudevents.sdk.converters import binary, structured +from cloudevents.sdk.event import v1, v03 from cloudevents.tests import data @pytest.mark.parametrize("event_class", [v03.Event, v1.Event]) def test_binary_converter_upstream(event_class): m = marshaller.NewHTTPMarshaller( - [binary.NewBinaryHTTPCloudEventConverter()]) + [binary.NewBinaryHTTPCloudEventConverter()] + ) event = m.FromRequest( - event_class(), - data.headers[event_class], - None, - lambda x: x + event_class(), data.headers[event_class], None, lambda x: x ) assert event is not None assert event.EventType() == data.ce_type @@ -47,7 +40,8 @@ def test_binary_converter_upstream(event_class): @pytest.mark.parametrize("event_class", [v03.Event, v1.Event]) def test_structured_converter_upstream(event_class): m = marshaller.NewHTTPMarshaller( - [structured.NewJSONHTTPCloudEventConverter()]) + [structured.NewJSONHTTPCloudEventConverter()] + ) event = m.FromRequest( event_class(), {"Content-Type": "application/cloudevents+json"}, @@ -82,9 +76,10 @@ def test_default_http_marshaller_with_binary(event_class): m = marshaller.NewDefaultHTTPMarshaller() event = m.FromRequest( - event_class(), data.headers[event_class], + event_class(), + data.headers[event_class], json.dumps(data.body), - json.loads + json.loads, ) assert event is not None assert event.EventType() == data.ce_type diff --git a/cloudevents/tests/test_event_pipeline.py b/cloudevents/tests/test_event_pipeline.py index 43fc3f7e..60da6e45 100644 --- a/cloudevents/tests/test_event_pipeline.py +++ b/cloudevents/tests/test_event_pipeline.py @@ -14,14 +14,12 @@ import io import json -import pytest -from cloudevents.sdk.event import v03, v1 +import pytest -from cloudevents.sdk import converters -from cloudevents.sdk import marshaller +from cloudevents.sdk import converters, marshaller from cloudevents.sdk.converters import structured - +from cloudevents.sdk.event import v1, v03 from cloudevents.tests import data @@ -51,11 +49,8 @@ def test_event_pipeline_upstream(event_class): def test_extensions_are_set_upstream(): - extensions = {'extension-key': 'extension-value'} - event = ( - v1.Event() - .SetExtensions(extensions) - ) + extensions = {"extension-key": "extension-value"} + event = v1.Event().SetExtensions(extensions) m = marshaller.NewDefaultHTTPMarshaller() new_headers, _ = m.ToRequest(event, converters.TypeBinary, lambda x: x) @@ -68,9 +63,11 @@ def test_binary_event_v1(): event = ( v1.Event() .SetContentType("application/octet-stream") - .SetData(b'\x00\x01') + .SetData(b"\x00\x01") + ) + m = marshaller.NewHTTPMarshaller( + [structured.NewJSONHTTPCloudEventConverter()] ) - m = marshaller.NewHTTPMarshaller([structured.NewJSONHTTPCloudEventConverter()]) _, body = m.ToRequest(event, converters.TypeStructured, lambda x: x) assert isinstance(body, bytes) @@ -78,11 +75,10 @@ def test_binary_event_v1(): assert "data" not in content assert content["data_base64"] == "AAE=", f"Content is: {content}" + def test_object_event_v1(): event = ( - v1.Event() - .SetContentType("application/json") - .SetData({"name": "john"}) + v1.Event().SetContentType("application/json").SetData({"name": "john"}) ) m = marshaller.NewDefaultHTTPMarshaller() diff --git a/cloudevents/tests/test_event_to_request_converter.py b/cloudevents/tests/test_event_to_request_converter.py index f46746b7..e54264f3 100644 --- a/cloudevents/tests/test_event_to_request_converter.py +++ b/cloudevents/tests/test_event_to_request_converter.py @@ -12,17 +12,15 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import io import json -import copy -import pytest -from cloudevents.sdk import converters -from cloudevents.sdk import marshaller +import pytest +from cloudevents.sdk import converters, marshaller from cloudevents.sdk.converters import structured -from cloudevents.sdk.event import v03, v1 - +from cloudevents.sdk.event import v1, v03 from cloudevents.tests import data @@ -51,9 +49,7 @@ def test_structured_event_to_request_upstream(event_class): m = marshaller.NewDefaultHTTPMarshaller() http_headers = {"content-type": "application/cloudevents+json"} event = m.FromRequest( - event_class(), - http_headers, - json.dumps(data.json_ce[event_class]) + event_class(), http_headers, json.dumps(data.json_ce[event_class]) ) assert event is not None assert event.EventType() == data.ce_type diff --git a/cloudevents/tests/test_http_events.py b/cloudevents/tests/test_http_events.py index 57822a54..6213edda 100644 --- a/cloudevents/tests/test_http_events.py +++ b/cloudevents/tests/test_http_events.py @@ -17,58 +17,47 @@ import io import json -from cloudevents.sdk.http_events import CloudEvent -from cloudevents.sdk import converters - -from sanic import response -from sanic import Sanic - import pytest +from sanic import Sanic, response +from cloudevents.sdk import converters +from cloudevents.sdk.http_events import CloudEvent invalid_test_headers = [ { "ce-source": "", "ce-type": "cloudevent.event.type", - "ce-specversion": "1.0" - }, { + "ce-specversion": "1.0", + }, + { "ce-id": "my-id", "ce-type": "cloudevent.event.type", - "ce-specversion": "1.0" - }, { - "ce-id": "my-id", - "ce-source": "", - "ce-specversion": "1.0" - }, { + "ce-specversion": "1.0", + }, + {"ce-id": "my-id", "ce-source": "", "ce-specversion": "1.0"}, + { "ce-id": "my-id", "ce-source": "", "ce-type": "cloudevent.event.type", - } + }, ] invalid_cloudevent_request_bodie = [ { "source": "", "type": "cloudevent.event.type", - "specversion": "1.0" - }, { - "id": "my-id", - "type": "cloudevent.event.type", - "specversion": "1.0" - }, { - "id": "my-id", - "source": "", - "specversion": "1.0" - }, { + "specversion": "1.0", + }, + {"id": "my-id", "type": "cloudevent.event.type", "specversion": "1.0"}, + {"id": "my-id", "source": "", "specversion": "1.0"}, + { "id": "my-id", "source": "", "type": "cloudevent.event.type", - } + }, ] -test_data = { - "payload-content": "Hello World!" -} +test_data = {"payload-content": "Hello World!"} app = Sanic(__name__) @@ -82,9 +71,14 @@ async def echo(request): decoder = None if "binary-payload" in request.headers: decoder = lambda x: x - event = CloudEvent.from_http(request.body, headers=dict(request.headers), data_unmarshaller=decoder) - data = event.data if isinstance( - event.data, (bytes, bytearray, memoryview)) else json.dumps(event.data).encode() + event = CloudEvent.from_http( + request.body, headers=dict(request.headers), data_unmarshaller=decoder + ) + data = ( + event.data + if isinstance(event.data, (bytes, bytearray, memoryview)) + else json.dumps(event.data).encode() + ) return response.raw(data, headers={k: event[k] for k in event}) @@ -95,8 +89,9 @@ def test_missing_required_fields_structured(body): # 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 - _ = CloudEvent.from_http(json.dumps(body), attributes={ - 'Content-Type': 'application/json'}) + _ = CloudEvent.from_http( + json.dumps(body), attributes={"Content-Type": "application/json"} + ) @pytest.mark.parametrize("headers", invalid_test_headers) @@ -109,70 +104,65 @@ def test_missing_required_fields_binary(headers): _ = CloudEvent.from_http(json.dumps(test_data), headers=headers) -@pytest.mark.parametrize("specversion", ['1.0', '0.3']) +@pytest.mark.parametrize("specversion", ["1.0", "0.3"]) def test_emit_binary_event(specversion): headers = { "ce-id": "my-id", "ce-source": "", "ce-type": "cloudevent.event.type", "ce-specversion": specversion, - "Content-Type": "text/plain" + "Content-Type": "text/plain", } data = json.dumps(test_data) - _, r = app.test_client.post( - "/event", - headers=headers, - data=data - ) + _, r = app.test_client.post("/event", headers=headers, data=data) # Convert byte array to dict # e.g. r.body = b'{"payload-content": "Hello World!"}' - body = json.loads(r.body.decode('utf-8')) + body = json.loads(r.body.decode("utf-8")) # Check response fields for key in test_data: assert body[key] == test_data[key], body for key in headers: - if key != 'Content-Type': + if key != "Content-Type": attribute_key = key[3:] assert r.headers[attribute_key] == headers[key] assert r.status_code == 200 -@pytest.mark.parametrize("specversion", ['1.0', '0.3']) +@pytest.mark.parametrize("specversion", ["1.0", "0.3"]) def test_emit_structured_event(specversion): - headers = { - "Content-Type": "application/cloudevents+json" - } + headers = {"Content-Type": "application/cloudevents+json"} body = { "id": "my-id", "source": "", "type": "cloudevent.event.type", "specversion": specversion, - "data": test_data + "data": test_data, } _, r = app.test_client.post( - "/event", - headers=headers, - data=json.dumps(body) + "/event", headers=headers, data=json.dumps(body) ) # Convert byte array to dict # e.g. r.body = b'{"payload-content": "Hello World!"}' - body = json.loads(r.body.decode('utf-8')) + body = json.loads(r.body.decode("utf-8")) # Check response fields for key in test_data: assert body[key] == test_data[key] assert r.status_code == 200 -@pytest.mark.parametrize("converter", [converters.TypeStructured, converters.TypeStructured]) + +@pytest.mark.parametrize( + "converter", [converters.TypeStructured, converters.TypeStructured] +) @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) def test_roundtrip_non_json_event(converter, specversion): input_data = io.BytesIO() for i in range(100): for j in range(20): - assert 1 == input_data.write(j.to_bytes(1, byteorder='big')) + assert 1 == input_data.write(j.to_bytes(1, byteorder="big")) compressed_data = bz2.compress(input_data.getvalue()) attrs = {"source": "test", "type": "t"} @@ -186,14 +176,15 @@ def test_roundtrip_non_json_event(converter, specversion): assert r.headers[key] == attrs[key] assert compressed_data == r.body, r.body -@pytest.mark.parametrize("specversion", ['1.0', '0.3']) + +@pytest.mark.parametrize("specversion", ["1.0", "0.3"]) def test_missing_ce_prefix_binary_event(specversion): prefixed_headers = {} headers = { "ce-id": "my-id", "ce-source": "", "ce-type": "cloudevent.event.type", - "ce-specversion": specversion + "ce-specversion": specversion, } for key in headers: @@ -208,7 +199,7 @@ def test_missing_ce_prefix_binary_event(specversion): _ = CloudEvent.from_http(test_data, headers=prefixed_headers) -@pytest.mark.parametrize("specversion", ['1.0', '0.3']) +@pytest.mark.parametrize("specversion", ["1.0", "0.3"]) def test_valid_binary_events(specversion): # Test creating multiple cloud events events_queue = [] @@ -219,21 +210,22 @@ def test_valid_binary_events(specversion): "ce-id": f"id{i}", "ce-source": f"source{i}.com.test", "ce-type": f"cloudevent.test.type", - "ce-specversion": specversion + "ce-specversion": specversion, } - data = {'payload': f"payload-{i}"} - events_queue.append(CloudEvent.from_http( - json.dumps(data), headers=headers)) + data = {"payload": f"payload-{i}"} + events_queue.append( + CloudEvent.from_http(json.dumps(data), headers=headers) + ) for i, event in enumerate(events_queue): data = event.data - assert event['id'] == f"id{i}" - assert event['source'] == f"source{i}.com.test" - assert event['specversion'] == specversion - assert event.data['payload'] == f"payload-{i}" + assert event["id"] == f"id{i}" + assert event["source"] == f"source{i}.com.test" + assert event["specversion"] == specversion + assert event.data["payload"] == f"payload-{i}" -@pytest.mark.parametrize("specversion", ['1.0', '0.3']) +@pytest.mark.parametrize("specversion", ["1.0", "0.3"]) def test_structured_to_request(specversion): attributes = { "specversion": specversion, @@ -248,23 +240,21 @@ def test_structured_to_request(specversion): assert isinstance(body_bytes, bytes) body = json.loads(body_bytes) - assert headers['content-type'] == 'application/cloudevents+json' + assert headers["content-type"] == "application/cloudevents+json" for key in attributes: assert body[key] == attributes[key] assert body["data"] == data, f"|{body_bytes}|| {body}" -@pytest.mark.parametrize("specversion", ['1.0', '0.3']) +@pytest.mark.parametrize("specversion", ["1.0", "0.3"]) def test_binary_to_request(specversion): attributes = { "specversion": specversion, "type": "word.found.name", "id": "96fb5f0b-001e-0108-6dfe-da6e2806f124", - "source": "pytest" - } - data = { - "message": "Hello World!" + "source": "pytest", } + data = {"message": "Hello World!"} event = CloudEvent(attributes, data) headers, body_bytes = event.to_http(converters.TypeBinary) body = json.loads(body_bytes) @@ -272,10 +262,10 @@ def test_binary_to_request(specversion): for key in data: assert body[key] == data[key] for key in attributes: - assert attributes[key] == headers['ce-' + key] + assert attributes[key] == headers["ce-" + key] -@pytest.mark.parametrize("specversion", ['1.0', '0.3']) +@pytest.mark.parametrize("specversion", ["1.0", "0.3"]) def test_empty_data_structured_event(specversion): # Testing if cloudevent breaks when no structured data field present attributes = { @@ -287,11 +277,12 @@ def test_empty_data_structured_event(specversion): "source": "", } - _ = CloudEvent.from_http(json.dumps(attributes), { - "content-type": "application/cloudevents+json"}) + _ = CloudEvent.from_http( + json.dumps(attributes), {"content-type": "application/cloudevents+json"} + ) -@pytest.mark.parametrize("specversion", ['1.0', '0.3']) +@pytest.mark.parametrize("specversion", ["1.0", "0.3"]) def test_empty_data_binary_event(specversion): # Testing if cloudevent breaks when no structured data field present headers = { @@ -302,10 +293,10 @@ def test_empty_data_binary_event(specversion): "ce-time": "2018-10-23T12:28:22.4579346Z", "ce-source": "", } - _ = CloudEvent.from_http('', headers) + _ = CloudEvent.from_http("", headers) -@pytest.mark.parametrize("specversion", ['1.0', '0.3']) +@pytest.mark.parametrize("specversion", ["1.0", "0.3"]) def test_valid_structured_events(specversion): # Test creating multiple cloud events events_queue = [] @@ -317,13 +308,17 @@ def test_valid_structured_events(specversion): "source": f"source{i}.com.test", "type": f"cloudevent.test.type", "specversion": specversion, - "data": {'payload': f"payload-{i}"} + "data": {"payload": f"payload-{i}"}, } - events_queue.append(CloudEvent.from_http(json.dumps(event), - {"content-type": "application/cloudevents+json"})) + events_queue.append( + CloudEvent.from_http( + json.dumps(event), + {"content-type": "application/cloudevents+json"}, + ) + ) for i, event in enumerate(events_queue): - assert event['id'] == f"id{i}" - assert event['source'] == f"source{i}.com.test" - assert event['specversion'] == specversion - assert event.data['payload'] == f"payload-{i}" + assert event["id"] == f"id{i}" + assert event["source"] == f"source{i}.com.test" + assert event["specversion"] == specversion + assert event.data["payload"] == f"payload-{i}" diff --git a/cloudevents/tests/test_with_sanic.py b/cloudevents/tests/test_with_sanic.py index 2fd99337..135bfd5c 100644 --- a/cloudevents/tests/test_with_sanic.py +++ b/cloudevents/tests/test_with_sanic.py @@ -12,38 +12,26 @@ # License for the specific language governing permissions and limitations # under the License. -from cloudevents.sdk import marshaller -from cloudevents.sdk import converters -from cloudevents.sdk.event import v1 - -from sanic import Sanic -from sanic import response +from sanic import Sanic, response +from cloudevents.sdk import converters, marshaller +from cloudevents.sdk.event import v1 from cloudevents.tests import data as test_data - m = marshaller.NewDefaultHTTPMarshaller() app = Sanic(__name__) @app.route("/is-ok", ["POST"]) async def is_ok(request): - m.FromRequest( - v1.Event(), - dict(request.headers), - request.body, - lambda x: x - ) + m.FromRequest(v1.Event(), dict(request.headers), request.body, lambda x: x) return response.text("OK") @app.route("/echo", ["POST"]) async def echo(request): event = m.FromRequest( - v1.Event(), - dict(request.headers), - request.body, - lambda x: x + v1.Event(), dict(request.headers), request.body, lambda x: x ) hs, body = m.ToRequest(event, converters.TypeBinary, lambda x: x) return response.text(body, headers=hs) @@ -66,7 +54,8 @@ def test_web_app_integration(): def test_web_app_echo(): _, r = app.test_client.post( - "/echo", headers=test_data.headers[v1.Event], data=test_data.body) + "/echo", headers=test_data.headers[v1.Event], data=test_data.body + ) assert r.status == 200 event = m.FromRequest(v1.Event(), dict(r.headers), r.body, lambda x: x) assert event is not None diff --git a/etc/docs_conf/conf.py b/etc/docs_conf/conf.py index 3f7eb417..9ccef129 100644 --- a/etc/docs_conf/conf.py +++ b/etc/docs_conf/conf.py @@ -19,14 +19,14 @@ # -- Project information ----------------------------------------------------- -project = 'CloudEvents Python SDK' -copyright = '2018, Denis Makogon' -author = 'Denis Makogon' +project = "CloudEvents Python SDK" +copyright = "2018, Denis Makogon" +author = "Denis Makogon" # The short X.Y version -version = '' +version = "" # The full version, including alpha/beta/rc tags -release = '' +release = "" # -- General configuration --------------------------------------------------- @@ -39,21 +39,21 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.mathjax', + "sphinx.ext.autodoc", + "sphinx.ext.mathjax", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['docstemplates'] +templates_path = ["docstemplates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -76,7 +76,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'pyramid' +html_theme = "pyramid" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -87,7 +87,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['docsstatic'] +html_static_path = ["docsstatic"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -103,7 +103,7 @@ # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'CloudEventsPythonSDKdoc' +htmlhelp_basename = "CloudEventsPythonSDKdoc" # -- Options for LaTeX output ------------------------------------------------ @@ -112,15 +112,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -130,8 +127,13 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'CloudEventsPythonSDK.tex', 'CloudEvents Python SDK Documentation', - 'Denis Makogon', 'manual'), + ( + master_doc, + "CloudEventsPythonSDK.tex", + "CloudEvents Python SDK Documentation", + "Denis Makogon", + "manual", + ), ] @@ -140,8 +142,13 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'cloudeventspythonsdk', 'CloudEvents Python SDK Documentation', - [author], 1) + ( + master_doc, + "cloudeventspythonsdk", + "CloudEvents Python SDK Documentation", + [author], + 1, + ) ] @@ -151,9 +158,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'CloudEventsPythonSDK', 'CloudEvents Python SDK Documentation', - author, 'CloudEventsPythonSDK', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "CloudEventsPythonSDK", + "CloudEvents Python SDK Documentation", + author, + "CloudEventsPythonSDK", + "One line description of project.", + "Miscellaneous", + ), ] @@ -172,7 +185,7 @@ # epub_uid = '' # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] -# -- Extension configuration ------------------------------------------------- \ No newline at end of file +# -- Extension configuration ------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..672bf5c9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[tool.black] +line-length = 80 +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' diff --git a/samples/http-cloudevents/client.py b/samples/http-cloudevents/client.py index 5e6d7a15..ce81c2c3 100644 --- a/samples/http-cloudevents/client.py +++ b/samples/http-cloudevents/client.py @@ -11,13 +11,14 @@ # 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 sys import io -from cloudevents.sdk.http_events import CloudEvent -from cloudevents.sdk import converters +import sys import requests +from cloudevents.sdk import converters +from cloudevents.sdk.http_events import CloudEvent + def send_binary_cloud_event(url): # define cloudevents data @@ -27,20 +28,17 @@ def send_binary_cloud_event(url): "type": "template.http.binary", "dataontenttype": "application/json", # Time will be filled in automatically if not set - "time": "2018-10-23T12:28:23.3464579Z" + "time": "2018-10-23T12:28:23.3464579Z", } data = {"payload-content": "Hello World!"} - # create a CloudEvent + # create a CloudEvent event = CloudEvent(attributes, data) headers, body = event.to_http(converters.TypeBinary) # send and print event requests.post(url, headers=headers, json=body) - print( - f"Sent {event['id']} from {event['source']} with " - f"{event.data}" - ) + print(f"Sent {event['id']} from {event['source']} with " f"{event.data}") def send_structured_cloud_event(url): @@ -55,24 +53,22 @@ def send_structured_cloud_event(url): } data = {"payload-content": "Hello World!"} - # create a CloudEvent + # create a CloudEvent event = CloudEvent(attributes, data) headers, body = event.to_request() # send and print event requests.post(url, headers=headers, json=body) - print( - f"Sent {event['id']} from {event['source']} with " - f"{event.data}" - ) + print(f"Sent {event['id']} from {event['source']} with " f"{event.data}") if __name__ == "__main__": # expects a url from command line. # e.g. python3 client.py http://localhost:3000/ if len(sys.argv) < 2: - sys.exit("Usage: python with_requests.py " - "") + sys.exit( + "Usage: python with_requests.py " "" + ) url = sys.argv[1] send_binary_cloud_event(url) diff --git a/samples/http-cloudevents/server.py b/samples/http-cloudevents/server.py index 3797239e..e342dab8 100644 --- a/samples/http-cloudevents/server.py +++ b/samples/http-cloudevents/server.py @@ -11,13 +11,15 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -from cloudevents.sdk.http_events import CloudEvent from flask import Flask, request + +from cloudevents.sdk.http_events import CloudEvent + app = Flask(__name__) # create an endpoint at http://localhost:/3000/ -@app.route('/', methods=['POST']) +@app.route("/", methods=["POST"]) def home(): # convert headers to dict print(request.get_data()) @@ -27,8 +29,8 @@ def home(): # print the received CloudEvent print(f"Received CloudEvent {event}") - return '', 204 + return "", 204 -if __name__ == '__main__': +if __name__ == "__main__": app.run(port=3000) diff --git a/samples/python-requests/cloudevent_to_request.py b/samples/python-requests/cloudevent_to_request.py index 0ae1d113..3df3f33c 100644 --- a/samples/python-requests/cloudevent_to_request.py +++ b/samples/python-requests/cloudevent_to_request.py @@ -13,25 +13,24 @@ # under the License. import json -import requests import sys -from cloudevents.sdk import converters -from cloudevents.sdk import marshaller +import requests +from cloudevents.sdk import converters, marshaller from cloudevents.sdk.event import v1 def run_binary(event, url): binary_headers, binary_data = http_marshaller.ToRequest( - event, converters.TypeBinary, json.dumps) + 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) - response = requests.post( - url, headers=binary_headers, data=binary_data) + response = requests.post(url, headers=binary_headers, data=binary_data) response.raise_for_status() @@ -42,30 +41,32 @@ def run_structured(event, url): print("structured CloudEvent") print(structured_data.getvalue()) - response = requests.post(url, - headers=structured_headers, - data=structured_data.getvalue()) + response = requests.post( + url, headers=structured_headers, data=structured_data.getvalue() + ) response.raise_for_status() if __name__ == "__main__": if len(sys.argv) < 3: - sys.exit("Usage: python with_requests.py " - "[binary | structured] " - "") + sys.exit( + "Usage: python with_requests.py " + "[binary | structured] " + "" + ) fmt = sys.argv[1] url = sys.argv[2] http_marshaller = marshaller.NewDefaultHTTPMarshaller() event = ( - v1.Event(). - SetContentType("application/json"). - SetData({"name": "denis"}). - SetEventID("my-id"). - SetSource("") + sys.exit("Usage: python with_requests.py " "") url = sys.argv[1] response = requests.get(url) @@ -35,7 +33,6 @@ data = io.BytesIO(response.content) event = v1.Event() http_marshaller = marshaller.NewDefaultHTTPMarshaller() - event = http_marshaller.FromRequest( - event, headers, data, json.load) + event = http_marshaller.FromRequest(event, headers, data, json.load) print(json.dumps(event.Properties())) diff --git a/setup.py b/setup.py index b242731f..93335285 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,4 @@ import setuptools -setuptools.setup( - setup_requires=['pbr>=2.0.0'], - pbr=True) +setuptools.setup(setup_requires=["pbr>=2.0.0"], pbr=True) diff --git a/tox.ini b/tox.ini index b3a89f9c..e975ea4c 100644 --- a/tox.ini +++ b/tox.ini @@ -11,13 +11,21 @@ setenv = PYTESTARGS = -v -s --tb=long --cov=cloudevents commands = pytest {env:PYTESTARGS} {posargs} +[testenv:reformat] +basepython=python3.8 +deps = + black + isort +commands = + black . + isort cloudevents samples + [testenv:lint] basepython = python3.8 +deps = + black + isort commands = - flake8 + black --check . + isort -c cloudevents samples -[flake8] -# See https://gitlab.com/pycqa/flake8/-/issues/466 for extend-ignore vs ignore -extend-ignore = H405,H404,H403,H401,H306,S101,N802,N803,N806,I202,I201 -show-source = True -exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,docs,venv,.venv,docs,etc,samples,tests