diff --git a/README.md b/README.md index 80158165..bee33e3c 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Below we will provide samples on how to send cloudevents using the popular ### Binary HTTP CloudEvent ```python -from cloudevents.sdk.http import CloudEvent, to_binary_http +from cloudevents.http import CloudEvent, to_binary_http import requests @@ -43,7 +43,7 @@ requests.post("", data=body, headers=headers) ### Structured HTTP CloudEvent ```python -from cloudevents.sdk.http import CloudEvent, to_structured_http +from cloudevents.http import CloudEvent, to_structured_http import requests @@ -70,7 +70,7 @@ The code below shows how to consume a cloudevent using the popular python web fr ```python from flask import Flask, request -from cloudevents.sdk.http import from_http +from cloudevents.http import from_http app = Flask(__name__) diff --git a/cloudevents/http/__init__.py b/cloudevents/http/__init__.py new file mode 100644 index 00000000..80fc5a74 --- /dev/null +++ b/cloudevents/http/__init__.py @@ -0,0 +1,23 @@ +# 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 json +import typing + +from cloudevents.http.event import CloudEvent +from cloudevents.http.http_methods import ( + from_http, + to_binary_http, + to_structured_http, +) +from cloudevents.http.json_methods import from_json, to_json diff --git a/cloudevents/sdk/http/event.py b/cloudevents/http/event.py similarity index 50% rename from cloudevents/sdk/http/event.py rename to cloudevents/http/event.py index c7d4b0a8..a31b894b 100644 --- a/cloudevents/sdk/http/event.py +++ b/cloudevents/http/event.py @@ -13,39 +13,13 @@ # under the License. import datetime -import json import typing import uuid -from cloudevents.sdk import converters, marshaller, types -from cloudevents.sdk.converters import is_binary -from cloudevents.sdk.event import v1, v03 -from cloudevents.sdk.marshaller import HTTPMarshaller +from cloudevents.http.mappings import _required_by_version -def default_marshaller(content: any): - if content is None or len(content) == 0: - return None - try: - return json.dumps(content) - except TypeError: - return content - - -_marshaller_by_format = { - converters.TypeStructured: lambda x: x, - converters.TypeBinary: default_marshaller, -} - -_obj_by_version = {"1.0": v1.Event, "0.3": v03.Event} - -_required_by_version = { - "1.0": v1.Event._ce_required_fields, - "0.3": v03.Event._ce_required_fields, -} - - -class EventClass: +class CloudEvent: """ Python-friendly cloudevent class supporting v1 events Supports both binary and structured mode CloudEvents @@ -114,71 +88,5 @@ def __len__(self): def __contains__(self, key): return key in self._attributes - -def _to_http( - event: EventClass, - format: str = converters.TypeStructured, - data_marshaller: types.MarshallerType = None, -) -> (dict, typing.Union[bytes, str]): - """ - Returns a tuple of HTTP headers/body dicts representing this cloudevent - - :param format: constant specifying an encoding format - :type format: str - :param data_marshaller: Callable function to cast event.data into - either a string or bytes - :type data_marshaller: types.MarshallerType - :returns: (http_headers: dict, http_body: bytes or str) - """ - if data_marshaller is None: - data_marshaller = _marshaller_by_format[format] - - if event._attributes["specversion"] not in _obj_by_version: - raise ValueError( - f"Unsupported specversion: {event._attributes['specversion']}" - ) - - event_handler = _obj_by_version[event._attributes["specversion"]]() - for k, v in event._attributes.items(): - event_handler.Set(k, v) - event_handler.data = event.data - - return marshaller.NewDefaultHTTPMarshaller().ToRequest( - event_handler, format, data_marshaller=data_marshaller - ) - - -def to_structured_http( - event: EventClass, data_marshaller: types.MarshallerType = None, -) -> (dict, typing.Union[bytes, str]): - """ - Returns a tuple of HTTP headers/body dicts representing this cloudevent - - :param event: CloudEvent to cast into http data - :type event: CloudEvent - :param data_marshaller: Callable function to cast event.data into - either a string or bytes - :type data_marshaller: types.MarshallerType - :returns: (http_headers: dict, http_body: bytes or str) - """ - return _to_http(event=event, data_marshaller=data_marshaller) - - -def to_binary_http( - event: EventClass, data_marshaller: types.MarshallerType = None, -) -> (dict, typing.Union[bytes, str]): - """ - Returns a tuple of HTTP headers/body dicts representing this cloudevent - - :param event: CloudEvent to cast into http data - :type event: CloudEvent - :param data_marshaller: Callable function to cast event.data into - either a string or bytes - :type data_marshaller: types.UnmarshallerType - :returns: (http_headers: dict, http_body: bytes or str) - """ - return _to_http( - event=event, - format=converters.TypeBinary, - data_marshaller=data_marshaller, - ) + def __repr__(self): + return str({"attributes": self._attributes, "data": self.data}) diff --git a/cloudevents/http/http_methods.py b/cloudevents/http/http_methods.py new file mode 100644 index 00000000..113e1969 --- /dev/null +++ b/cloudevents/http/http_methods.py @@ -0,0 +1,121 @@ +import json +import typing + +from cloudevents.http.event import CloudEvent +from cloudevents.http.mappings import _marshaller_by_format, _obj_by_version +from cloudevents.http.util import _json_or_string +from cloudevents.sdk import converters, marshaller, types + + +def from_http( + data: typing.Union[str, bytes], + headers: typing.Dict[str, str], + 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_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_unmarshaller is None: + data_unmarshaller = _json_or_string + + marshall = marshaller.NewDefaultHTTPMarshaller() + + if converters.is_binary(headers): + specversion = headers.get("ce-specversion", None) + else: + raw_ce = json.loads(data) + specversion = raw_ce.get("specversion", None) + + if specversion is None: + raise ValueError("could not find specversion in HTTP request") + + event_handler = _obj_by_version.get(specversion, None) + + if event_handler is None: + raise ValueError(f"found invalid specversion {specversion}") + + event = marshall.FromRequest( + event_handler(), headers, data, data_unmarshaller=data_unmarshaller + ) + attrs = event.Properties() + attrs.pop("data", None) + attrs.pop("extensions", None) + attrs.update(**event.extensions) + + return CloudEvent(attrs, event.data) + + +def _to_http( + event: CloudEvent, + format: str = converters.TypeStructured, + data_marshaller: types.MarshallerType = None, +) -> (dict, typing.Union[bytes, str]): + """ + Returns a tuple of HTTP headers/body dicts representing this cloudevent + + :param format: constant specifying an encoding format + :type format: str + :param data_marshaller: Callable function to cast event.data into + either a string or bytes + :type data_marshaller: types.MarshallerType + :returns: (http_headers: dict, http_body: bytes or str) + """ + if data_marshaller is None: + data_marshaller = _marshaller_by_format[format] + + if event._attributes["specversion"] not in _obj_by_version: + raise ValueError( + f"Unsupported specversion: {event._attributes['specversion']}" + ) + + event_handler = _obj_by_version[event._attributes["specversion"]]() + for k, v in event._attributes.items(): + event_handler.Set(k, v) + event_handler.data = event.data + + return marshaller.NewDefaultHTTPMarshaller().ToRequest( + event_handler, format, data_marshaller=data_marshaller + ) + + +def to_structured_http( + event: CloudEvent, data_marshaller: types.MarshallerType = None, +) -> (dict, typing.Union[bytes, str]): + """ + Returns a tuple of HTTP headers/body dicts representing this cloudevent + + :param event: CloudEvent to cast into http data + :type event: CloudEvent + :param data_marshaller: Callable function to cast event.data into + either a string or bytes + :type data_marshaller: types.MarshallerType + :returns: (http_headers: dict, http_body: bytes or str) + """ + return _to_http(event=event, data_marshaller=data_marshaller) + + +def to_binary_http( + event: CloudEvent, data_marshaller: types.MarshallerType = None, +) -> (dict, typing.Union[bytes, str]): + """ + Returns a tuple of HTTP headers/body dicts representing this cloudevent + + :param event: CloudEvent to cast into http data + :type event: CloudEvent + :param data_marshaller: Callable function to cast event.data into + either a string or bytes + :type data_marshaller: types.UnmarshallerType + :returns: (http_headers: dict, http_body: bytes or str) + """ + return _to_http( + event=event, + format=converters.TypeBinary, + data_marshaller=data_marshaller, + ) diff --git a/cloudevents/http/json_methods.py b/cloudevents/http/json_methods.py new file mode 100644 index 00000000..8d6bfdd6 --- /dev/null +++ b/cloudevents/http/json_methods.py @@ -0,0 +1,36 @@ +import typing + +from cloudevents.http.event import CloudEvent +from cloudevents.http.http_methods import from_http, to_structured_http +from cloudevents.sdk import types + + +def to_json( + event: CloudEvent, data_marshaller: types.MarshallerType = None +) -> typing.Union[str, bytes]: + """ + Cast an CloudEvent into a json object + :param event: CloudEvent which will be converted into a json object + :type event: CloudEvent + :param data_marshaller: Callable function which will cast event.data + into a json object + :type data_marshaller: typing.Callable + :returns: json object representing the given event + """ + return to_structured_http(event, data_marshaller=data_marshaller)[1] + + +def from_json( + data: typing.Union[str, bytes], + data_unmarshaller: types.UnmarshallerType = None, +) -> CloudEvent: + """ + Cast json encoded data into an CloudEvent + :param data: json encoded cloudevent data + :type event: typing.Union[str, bytes] + :param data_unmarshaller: Callable function which will cast data to a + python object + :type data_unmarshaller: typing.Callable + :returns: CloudEvent representing given cloudevent json object + """ + return from_http(data=data, headers={}, data_unmarshaller=data_unmarshaller) diff --git a/cloudevents/http/mappings.py b/cloudevents/http/mappings.py new file mode 100644 index 00000000..4a85175c --- /dev/null +++ b/cloudevents/http/mappings.py @@ -0,0 +1,15 @@ +from cloudevents.http.util import default_marshaller +from cloudevents.sdk import converters +from cloudevents.sdk.event import v1, v03 + +_marshaller_by_format = { + converters.TypeStructured: lambda x: x, + converters.TypeBinary: default_marshaller, +} + +_obj_by_version = {"1.0": v1.Event, "0.3": v03.Event} + +_required_by_version = { + "1.0": v1.Event._ce_required_fields, + "0.3": v03.Event._ce_required_fields, +} diff --git a/cloudevents/http/util.py b/cloudevents/http/util.py new file mode 100644 index 00000000..d641df7a --- /dev/null +++ b/cloudevents/http/util.py @@ -0,0 +1,20 @@ +import json +import typing + + +def default_marshaller(content: any): + if content is None or len(content) == 0: + return None + try: + return json.dumps(content) + except TypeError: + return content + + +def _json_or_string(content: typing.Union[str, bytes]): + if len(content) == 0: + return None + try: + return json.loads(content) + except (json.JSONDecodeError, TypeError) as e: + return content diff --git a/cloudevents/sdk/http/__init__.py b/cloudevents/sdk/http/__init__.py deleted file mode 100644 index 90904024..00000000 --- a/cloudevents/sdk/http/__init__.py +++ /dev/null @@ -1,114 +0,0 @@ -# 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 json -import typing - -from cloudevents.sdk import converters, marshaller, types -from cloudevents.sdk.event import v1, v03 -from cloudevents.sdk.http.event import ( - EventClass, - _obj_by_version, - to_binary_http, - to_structured_http, -) - - -class CloudEvent(EventClass): - def __repr__(self): - return to_structured_http(self)[1].decode() - - -def _json_or_string(content: typing.Union[str, bytes]): - if len(content) == 0: - return None - try: - return json.loads(content) - except (json.JSONDecodeError, TypeError) as e: - return content - - -def from_http( - data: typing.Union[str, bytes], - headers: typing.Dict[str, str], - 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_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_unmarshaller is None: - data_unmarshaller = _json_or_string - - marshall = marshaller.NewDefaultHTTPMarshaller() - - if converters.is_binary(headers): - specversion = headers.get("ce-specversion", None) - else: - raw_ce = json.loads(data) - specversion = raw_ce.get("specversion", None) - - if specversion is None: - raise ValueError("could not find specversion in HTTP request") - - event_handler = _obj_by_version.get(specversion, None) - - if event_handler is None: - raise ValueError(f"found invalid specversion {specversion}") - - event = marshall.FromRequest( - event_handler(), headers, data, data_unmarshaller=data_unmarshaller - ) - attrs = event.Properties() - attrs.pop("data", None) - attrs.pop("extensions", None) - attrs.update(**event.extensions) - - return CloudEvent(attrs, event.data) - - -def to_json( - event: EventClass, data_marshaller: types.MarshallerType = None -) -> typing.Union[str, bytes]: - """ - Cast an EventClass into a json object - :param event: EventClass which will be converted into a json object - :type event: EventClass - :param data_marshaller: Callable function which will cast event.data - into a json object - :type data_marshaller: typing.Callable - :returns: json object representing the given event - """ - return to_structured_http(event, data_marshaller=data_marshaller)[1] - - -def from_json( - data: typing.Union[str, bytes], - data_unmarshaller: types.UnmarshallerType = None, -) -> EventClass: - """ - Cast json encoded data into an EventClass - :param data: json encoded cloudevent data - :type event: typing.Union[str, bytes] - :param data_unmarshaller: Callable function which will cast data to a - python object - :type data_unmarshaller: typing.Callable - :returns: EventClass representing given cloudevent json object - """ - return from_http(data=data, headers={}, data_unmarshaller=data_unmarshaller) diff --git a/cloudevents/tests/test_event_extensions.py b/cloudevents/tests/test_event_extensions.py index 57afff47..b9731ab3 100644 --- a/cloudevents/tests/test_event_extensions.py +++ b/cloudevents/tests/test_event_extensions.py @@ -15,7 +15,7 @@ import pytest -from cloudevents.sdk.http import ( +from cloudevents.http import ( CloudEvent, from_http, to_binary_http, diff --git a/cloudevents/tests/test_http_events.py b/cloudevents/tests/test_http_events.py index 140fb823..883e01b8 100644 --- a/cloudevents/tests/test_http_events.py +++ b/cloudevents/tests/test_http_events.py @@ -20,13 +20,13 @@ import pytest from sanic import Sanic, response -from cloudevents.sdk import converters -from cloudevents.sdk.http import ( +from cloudevents.http import ( CloudEvent, from_http, to_binary_http, to_structured_http, ) +from cloudevents.sdk import converters invalid_test_headers = [ { diff --git a/cloudevents/tests/test_http_json_methods.py b/cloudevents/tests/test_http_json_methods.py index 293a9efc..71074b19 100644 --- a/cloudevents/tests/test_http_json_methods.py +++ b/cloudevents/tests/test_http_json_methods.py @@ -16,7 +16,7 @@ import pytest -from cloudevents.sdk.http import CloudEvent, from_json, to_json +from cloudevents.http import CloudEvent, from_json, to_json test_data = json.dumps({"data-key": "val"}) test_attributes = { diff --git a/samples/http-image-cloudevents/client.py b/samples/http-image-cloudevents/client.py index 8b2fc695..3b856d1f 100644 --- a/samples/http-image-cloudevents/client.py +++ b/samples/http-image-cloudevents/client.py @@ -15,7 +15,7 @@ import requests -from cloudevents.sdk.http import CloudEvent, to_binary_http, to_structured_http +from cloudevents.http import CloudEvent, to_binary_http, to_structured_http resp = requests.get( "https://raw.githubusercontent.com/cncf/artwork/master/projects/cloudevents/horizontal/color/cloudevents-horizontal-color.png" diff --git a/samples/http-image-cloudevents/server.py b/samples/http-image-cloudevents/server.py index 7704a30a..07d9a892 100644 --- a/samples/http-image-cloudevents/server.py +++ b/samples/http-image-cloudevents/server.py @@ -16,7 +16,7 @@ from flask import Flask, Response, request from PIL import Image -from cloudevents.sdk.http import from_http +from cloudevents.http import from_http app = Flask(__name__) diff --git a/samples/http-image-cloudevents/test_image_sample.py b/samples/http-image-cloudevents/test_image_sample.py index 0fb295fb..5b1c4030 100644 --- a/samples/http-image-cloudevents/test_image_sample.py +++ b/samples/http-image-cloudevents/test_image_sample.py @@ -7,7 +7,7 @@ from PIL import Image from server import app -from cloudevents.sdk.http import ( +from cloudevents.http import ( CloudEvent, from_http, to_binary_http, diff --git a/samples/http-json-cloudevents/client.py b/samples/http-json-cloudevents/client.py index c7a507ad..a77fd33d 100644 --- a/samples/http-json-cloudevents/client.py +++ b/samples/http-json-cloudevents/client.py @@ -16,7 +16,7 @@ import requests -from cloudevents.sdk.http import CloudEvent, to_binary_http, to_structured_http +from cloudevents.http import CloudEvent, to_binary_http, to_structured_http def send_binary_cloud_event(url): diff --git a/samples/http-json-cloudevents/server.py b/samples/http-json-cloudevents/server.py index e1fbfda0..c36afc82 100644 --- a/samples/http-json-cloudevents/server.py +++ b/samples/http-json-cloudevents/server.py @@ -13,7 +13,7 @@ # under the License. from flask import Flask, request -from cloudevents.sdk.http import from_http +from cloudevents.http import from_http app = Flask(__name__)