Merge f6522d68d1 into c5645d8fcf
				
					
				
			This commit is contained in:
		
						commit
						22fcfaf366
					
				|  | @ -17,12 +17,12 @@ jobs: | ||||||
|       - name: Install dev dependencies |       - name: Install dev dependencies | ||||||
|         run: python -m pip install -r requirements/dev.txt |         run: python -m pip install -r requirements/dev.txt | ||||||
|       - name: Run linting |       - name: Run linting | ||||||
|         run: python -m tox -e lint |         run: python -m tox -e lint,mypy,mypy-samples-image,mypy-samples-json | ||||||
| 
 | 
 | ||||||
|   test: |   test: | ||||||
|     strategy: |     strategy: | ||||||
|       matrix: |       matrix: | ||||||
|         python: ['3.8', '3.9', '3.10', '3.11'] |         python: ['3.9', '3.10', '3.11', '3.12', '3.13'] | ||||||
|         os: [ubuntu-latest, windows-latest, macos-latest] |         os: [ubuntu-latest, windows-latest, macos-latest] | ||||||
|     runs-on: ${{ matrix.os }} |     runs-on: ${{ matrix.os }} | ||||||
|     steps: |     steps: | ||||||
|  |  | ||||||
|  | @ -32,7 +32,7 @@ class CloudEvent: | ||||||
|     @classmethod |     @classmethod | ||||||
|     def create( |     def create( | ||||||
|         cls: typing.Type[AnyCloudEvent], |         cls: typing.Type[AnyCloudEvent], | ||||||
|         attributes: typing.Dict[str, typing.Any], |         attributes: typing.Mapping[str, typing.Any], | ||||||
|         data: typing.Optional[typing.Any], |         data: typing.Optional[typing.Any], | ||||||
|     ) -> AnyCloudEvent: |     ) -> AnyCloudEvent: | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|  | @ -91,7 +91,9 @@ def from_json( | ||||||
| 
 | 
 | ||||||
| def from_http( | def from_http( | ||||||
|     event_type: typing.Type[AnyCloudEvent], |     event_type: typing.Type[AnyCloudEvent], | ||||||
|     headers: typing.Mapping[str, str], |     headers: typing.Union[ | ||||||
|  |         typing.Mapping[str, str], types.SupportsDuplicateItems[str, str] | ||||||
|  |     ], | ||||||
|     data: typing.Optional[typing.Union[str, bytes]], |     data: typing.Optional[typing.Union[str, bytes]], | ||||||
|     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, |     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, | ||||||
| ) -> AnyCloudEvent: | ) -> AnyCloudEvent: | ||||||
|  | @ -260,7 +262,7 @@ def best_effort_encode_attribute_value(value: typing.Any) -> typing.Any: | ||||||
| 
 | 
 | ||||||
| def from_dict( | def from_dict( | ||||||
|     event_type: typing.Type[AnyCloudEvent], |     event_type: typing.Type[AnyCloudEvent], | ||||||
|     event: typing.Dict[str, typing.Any], |     event: typing.Mapping[str, typing.Any], | ||||||
| ) -> AnyCloudEvent: | ) -> AnyCloudEvent: | ||||||
|     """ |     """ | ||||||
|     Constructs an Event object of a given `event_type` from |     Constructs an Event object of a given `event_type` from | ||||||
|  |  | ||||||
|  | @ -37,7 +37,9 @@ def from_json( | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def from_http( | def from_http( | ||||||
|     headers: typing.Dict[str, str], |     headers: typing.Union[ | ||||||
|  |         typing.Mapping[str, str], types.SupportsDuplicateItems[str, str] | ||||||
|  |     ], | ||||||
|     data: typing.Optional[typing.Union[str, bytes]], |     data: typing.Optional[typing.Union[str, bytes]], | ||||||
|     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, |     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, | ||||||
| ) -> CloudEvent: | ) -> CloudEvent: | ||||||
|  | @ -58,7 +60,7 @@ def from_http( | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def from_dict( | def from_dict( | ||||||
|     event: typing.Dict[str, typing.Any], |     event: typing.Mapping[str, typing.Any], | ||||||
| ) -> CloudEvent: | ) -> CloudEvent: | ||||||
|     """ |     """ | ||||||
|     Constructs a CloudEvent from a dict `event` representation. |     Constructs a CloudEvent from a dict `event` representation. | ||||||
|  |  | ||||||
|  | @ -34,11 +34,13 @@ class CloudEvent(abstract.CloudEvent): | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def create( |     def create( | ||||||
|         cls, attributes: typing.Dict[str, typing.Any], data: typing.Optional[typing.Any] |         cls, | ||||||
|  |         attributes: typing.Mapping[str, typing.Any], | ||||||
|  |         data: typing.Optional[typing.Any], | ||||||
|     ) -> "CloudEvent": |     ) -> "CloudEvent": | ||||||
|         return cls(attributes, data) |         return cls(attributes, data) | ||||||
| 
 | 
 | ||||||
|     def __init__(self, attributes: typing.Dict[str, str], data: typing.Any = None): |     def __init__(self, attributes: typing.Mapping[str, str], data: typing.Any = None): | ||||||
|         """ |         """ | ||||||
|         Event Constructor |         Event Constructor | ||||||
|         :param attributes: a dict with cloudevent attributes. Minimally |         :param attributes: a dict with cloudevent attributes. Minimally | ||||||
|  |  | ||||||
|  | @ -111,11 +111,29 @@ def to_binary( | ||||||
|     return KafkaMessage(headers, message_key, data) |     return KafkaMessage(headers, message_key, data) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @typing.overload | ||||||
|  | def from_binary( | ||||||
|  |     message: KafkaMessage, | ||||||
|  |     event_type: None = None, | ||||||
|  |     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, | ||||||
|  | ) -> http.CloudEvent: | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @typing.overload | ||||||
|  | def from_binary( | ||||||
|  |     message: KafkaMessage, | ||||||
|  |     event_type: typing.Type[AnyCloudEvent], | ||||||
|  |     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, | ||||||
|  | ) -> AnyCloudEvent: | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def from_binary( | def from_binary( | ||||||
|     message: KafkaMessage, |     message: KafkaMessage, | ||||||
|     event_type: typing.Optional[typing.Type[AnyCloudEvent]] = None, |     event_type: typing.Optional[typing.Type[AnyCloudEvent]] = None, | ||||||
|     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, |     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, | ||||||
| ) -> AnyCloudEvent: | ) -> typing.Union[http.CloudEvent, AnyCloudEvent]: | ||||||
|     """ |     """ | ||||||
|     Returns a CloudEvent from a KafkaMessage in binary format. |     Returns a CloudEvent from a KafkaMessage in binary format. | ||||||
| 
 | 
 | ||||||
|  | @ -144,10 +162,11 @@ def from_binary( | ||||||
|         raise cloud_exceptions.DataUnmarshallerError( |         raise cloud_exceptions.DataUnmarshallerError( | ||||||
|             f"Failed to unmarshall data with error: {type(e).__name__}('{e}')" |             f"Failed to unmarshall data with error: {type(e).__name__}('{e}')" | ||||||
|         ) |         ) | ||||||
|  |     result: typing.Union[http.CloudEvent, AnyCloudEvent] | ||||||
|     if event_type: |     if event_type: | ||||||
|         result = event_type.create(attributes, data) |         result = event_type.create(attributes, data) | ||||||
|     else: |     else: | ||||||
|         result = http.CloudEvent.create(attributes, data)  # type: ignore |         result = http.CloudEvent.create(attributes, data) | ||||||
|     return result |     return result | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -210,12 +229,32 @@ def to_structured( | ||||||
|     return KafkaMessage(headers, message_key, value) |     return KafkaMessage(headers, message_key, value) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @typing.overload | ||||||
|  | def from_structured( | ||||||
|  |     message: KafkaMessage, | ||||||
|  |     event_type: None = None, | ||||||
|  |     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, | ||||||
|  |     envelope_unmarshaller: typing.Optional[types.UnmarshallerType] = None, | ||||||
|  | ) -> http.CloudEvent: | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @typing.overload | ||||||
|  | def from_structured( | ||||||
|  |     message: KafkaMessage, | ||||||
|  |     event_type: typing.Type[AnyCloudEvent], | ||||||
|  |     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, | ||||||
|  |     envelope_unmarshaller: typing.Optional[types.UnmarshallerType] = None, | ||||||
|  | ) -> AnyCloudEvent: | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def from_structured( | def from_structured( | ||||||
|     message: KafkaMessage, |     message: KafkaMessage, | ||||||
|     event_type: typing.Optional[typing.Type[AnyCloudEvent]] = None, |     event_type: typing.Optional[typing.Type[AnyCloudEvent]] = None, | ||||||
|     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, |     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, | ||||||
|     envelope_unmarshaller: typing.Optional[types.UnmarshallerType] = None, |     envelope_unmarshaller: typing.Optional[types.UnmarshallerType] = None, | ||||||
| ) -> AnyCloudEvent: | ) -> typing.Union[http.CloudEvent, AnyCloudEvent]: | ||||||
|     """ |     """ | ||||||
|     Returns a CloudEvent from a KafkaMessage in structured format. |     Returns a CloudEvent from a KafkaMessage in structured format. | ||||||
| 
 | 
 | ||||||
|  | @ -264,8 +303,9 @@ def from_structured( | ||||||
|             attributes["datacontenttype"] = val.decode() |             attributes["datacontenttype"] = val.decode() | ||||||
|         else: |         else: | ||||||
|             attributes[header.lower()] = val.decode() |             attributes[header.lower()] = val.decode() | ||||||
|  |     result: typing.Union[AnyCloudEvent, http.CloudEvent] | ||||||
|     if event_type: |     if event_type: | ||||||
|         result = event_type.create(attributes, data) |         result = event_type.create(attributes, data) | ||||||
|     else: |     else: | ||||||
|         result = http.CloudEvent.create(attributes, data)  # type: ignore |         result = http.CloudEvent.create(attributes, data) | ||||||
|     return result |     return result | ||||||
|  |  | ||||||
|  | @ -21,7 +21,9 @@ from cloudevents.sdk import types | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def from_http( | def from_http( | ||||||
|     headers: typing.Dict[str, str], |     headers: typing.Union[ | ||||||
|  |         typing.Mapping[str, str], types.SupportsDuplicateItems[str, str] | ||||||
|  |     ], | ||||||
|     data: typing.Optional[typing.AnyStr], |     data: typing.Optional[typing.AnyStr], | ||||||
|     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, |     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, | ||||||
| ) -> CloudEvent: | ) -> CloudEvent: | ||||||
|  | @ -63,7 +65,7 @@ def from_json( | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def from_dict( | def from_dict( | ||||||
|     event: typing.Dict[str, typing.Any], |     event: typing.Mapping[str, typing.Any], | ||||||
| ) -> CloudEvent: | ) -> CloudEvent: | ||||||
|     """ |     """ | ||||||
|     Construct an CloudEvent from a dict `event` representation. |     Construct an CloudEvent from a dict `event` representation. | ||||||
|  |  | ||||||
|  | @ -100,7 +100,9 @@ class CloudEvent(abstract.CloudEvent, BaseModel):  # type: ignore | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def create( |     def create( | ||||||
|         cls, attributes: typing.Dict[str, typing.Any], data: typing.Optional[typing.Any] |         cls, | ||||||
|  |         attributes: typing.Mapping[str, typing.Any], | ||||||
|  |         data: typing.Optional[typing.Any], | ||||||
|     ) -> "CloudEvent": |     ) -> "CloudEvent": | ||||||
|         return cls(attributes, data) |         return cls(attributes, data) | ||||||
| 
 | 
 | ||||||
|  | @ -155,7 +157,7 @@ class CloudEvent(abstract.CloudEvent, BaseModel):  # type: ignore | ||||||
| 
 | 
 | ||||||
|     def __init__(  # type: ignore[no-untyped-def] |     def __init__(  # type: ignore[no-untyped-def] | ||||||
|         self, |         self, | ||||||
|         attributes: typing.Optional[typing.Dict[str, typing.Any]] = None, |         attributes: typing.Optional[typing.Mapping[str, typing.Any]] = None, | ||||||
|         data: typing.Optional[typing.Any] = None, |         data: typing.Optional[typing.Any] = None, | ||||||
|         **kwargs, |         **kwargs, | ||||||
|     ): |     ): | ||||||
|  |  | ||||||
|  | @ -22,7 +22,9 @@ from cloudevents.sdk import types | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def from_http( | def from_http( | ||||||
|     headers: typing.Dict[str, str], |     headers: typing.Union[ | ||||||
|  |         typing.Mapping[str, str], types.SupportsDuplicateItems[str, str] | ||||||
|  |     ], | ||||||
|     data: typing.Optional[typing.AnyStr], |     data: typing.Optional[typing.AnyStr], | ||||||
|     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, |     data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, | ||||||
| ) -> CloudEvent: | ) -> CloudEvent: | ||||||
|  | @ -64,7 +66,7 @@ def from_json( | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def from_dict( | def from_dict( | ||||||
|     event: typing.Dict[str, typing.Any], |     event: typing.Mapping[str, typing.Any], | ||||||
| ) -> CloudEvent: | ) -> CloudEvent: | ||||||
|     """ |     """ | ||||||
|     Construct an CloudEvent from a dict `event` representation. |     Construct an CloudEvent from a dict `event` representation. | ||||||
|  |  | ||||||
|  | @ -44,7 +44,9 @@ class CloudEvent(abstract.CloudEvent, BaseModel):  # type: ignore | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def create( |     def create( | ||||||
|         cls, attributes: typing.Dict[str, typing.Any], data: typing.Optional[typing.Any] |         cls, | ||||||
|  |         attributes: typing.Mapping[str, typing.Any], | ||||||
|  |         data: typing.Optional[typing.Any], | ||||||
|     ) -> "CloudEvent": |     ) -> "CloudEvent": | ||||||
|         return cls(attributes, data) |         return cls(attributes, data) | ||||||
| 
 | 
 | ||||||
|  | @ -103,7 +105,7 @@ class CloudEvent(abstract.CloudEvent, BaseModel):  # type: ignore | ||||||
| 
 | 
 | ||||||
|     def __init__(  # type: ignore[no-untyped-def] |     def __init__(  # type: ignore[no-untyped-def] | ||||||
|         self, |         self, | ||||||
|         attributes: typing.Optional[typing.Dict[str, typing.Any]] = None, |         attributes: typing.Optional[typing.Mapping[str, typing.Any]] = None, | ||||||
|         data: typing.Optional[typing.Any] = None, |         data: typing.Optional[typing.Any] = None, | ||||||
|         **kwargs, |         **kwargs, | ||||||
|     ): |     ): | ||||||
|  | @ -173,6 +175,8 @@ class CloudEvent(abstract.CloudEvent, BaseModel):  # type: ignore | ||||||
|         *, |         *, | ||||||
|         strict: typing.Optional[bool] = None, |         strict: typing.Optional[bool] = None, | ||||||
|         context: typing.Optional[typing.Dict[str, Any]] = None, |         context: typing.Optional[typing.Dict[str, Any]] = None, | ||||||
|  |         by_alias: typing.Optional[bool] = None, | ||||||
|  |         by_name: typing.Optional[bool] = None, | ||||||
|     ) -> "CloudEvent": |     ) -> "CloudEvent": | ||||||
|         return conversion.from_json(cls, json_data) |         return conversion.from_json(cls, json_data) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -11,10 +11,15 @@ | ||||||
| #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||||
| #    License for the specific language governing permissions and limitations | #    License for the specific language governing permissions and limitations | ||||||
| #    under the License. | #    under the License. | ||||||
|  | from __future__ import annotations | ||||||
|  | 
 | ||||||
| import typing | import typing | ||||||
| 
 | 
 | ||||||
| from cloudevents.sdk.event import base, opt | from cloudevents.sdk.event import base, opt | ||||||
| 
 | 
 | ||||||
|  | if typing.TYPE_CHECKING: | ||||||
|  |     from typing_extensions import Self | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class Event(base.BaseEvent): | class Event(base.BaseEvent): | ||||||
|     _ce_required_fields = {"id", "source", "type", "specversion"} |     _ce_required_fields = {"id", "source", "type", "specversion"} | ||||||
|  | @ -79,39 +84,39 @@ class Event(base.BaseEvent): | ||||||
|             return {} |             return {} | ||||||
|         return dict(result) |         return dict(result) | ||||||
| 
 | 
 | ||||||
|     def SetEventType(self, eventType: str) -> base.BaseEvent: |     def SetEventType(self, eventType: str) -> Self: | ||||||
|         self.Set("type", eventType) |         self.Set("type", eventType) | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def SetSource(self, source: str) -> base.BaseEvent: |     def SetSource(self, source: str) -> Self: | ||||||
|         self.Set("source", source) |         self.Set("source", source) | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def SetEventID(self, eventID: str) -> base.BaseEvent: |     def SetEventID(self, eventID: str) -> Self: | ||||||
|         self.Set("id", eventID) |         self.Set("id", eventID) | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def SetEventTime(self, eventTime: typing.Optional[str]) -> base.BaseEvent: |     def SetEventTime(self, eventTime: typing.Optional[str]) -> Self: | ||||||
|         self.Set("time", eventTime) |         self.Set("time", eventTime) | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def SetSubject(self, subject: typing.Optional[str]) -> base.BaseEvent: |     def SetSubject(self, subject: typing.Optional[str]) -> Self: | ||||||
|         self.Set("subject", subject) |         self.Set("subject", subject) | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def SetSchema(self, schema: typing.Optional[str]) -> base.BaseEvent: |     def SetSchema(self, schema: typing.Optional[str]) -> Self: | ||||||
|         self.Set("dataschema", schema) |         self.Set("dataschema", schema) | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def SetContentType(self, contentType: typing.Optional[str]) -> base.BaseEvent: |     def SetContentType(self, contentType: typing.Optional[str]) -> Self: | ||||||
|         self.Set("datacontenttype", contentType) |         self.Set("datacontenttype", contentType) | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def SetData(self, data: typing.Optional[object]) -> base.BaseEvent: |     def SetData(self, data: typing.Optional[object]) -> Self: | ||||||
|         self.Set("data", data) |         self.Set("data", data) | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|     def SetExtensions(self, extensions: typing.Optional[dict]) -> base.BaseEvent: |     def SetExtensions(self, extensions: typing.Optional[dict]) -> Self: | ||||||
|         self.Set("extensions", extensions) |         self.Set("extensions", extensions) | ||||||
|         return self |         return self | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -14,9 +14,25 @@ | ||||||
| 
 | 
 | ||||||
| import typing | import typing | ||||||
| 
 | 
 | ||||||
|  | _K_co = typing.TypeVar("_K_co", covariant=True) | ||||||
|  | _V_co = typing.TypeVar("_V_co", covariant=True) | ||||||
|  | 
 | ||||||
| # Use consistent types for marshal and unmarshal functions across | # Use consistent types for marshal and unmarshal functions across | ||||||
| # both JSON and Binary format. | # both JSON and Binary format. | ||||||
| 
 | 
 | ||||||
| MarshallerType = typing.Callable[[typing.Any], typing.AnyStr] | MarshallerType = typing.Callable[[typing.Any], typing.AnyStr] | ||||||
| 
 | 
 | ||||||
| UnmarshallerType = typing.Callable[[typing.AnyStr], typing.Any] | UnmarshallerType = typing.Callable[[typing.AnyStr], typing.Any] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class SupportsDuplicateItems(typing.Protocol[_K_co, _V_co]): | ||||||
|  |     """ | ||||||
|  |     Dict-like objects with an items() method that may produce duplicate keys. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     # This is wider than _typeshed.SupportsItems, which expects items() to | ||||||
|  |     # return type an AbstractSet. werkzeug's Headers class satisfies this type, | ||||||
|  |     # but not _typeshed.SupportsItems. | ||||||
|  | 
 | ||||||
|  |     def items(self) -> typing.Iterable[typing.Tuple[_K_co, _V_co]]: | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  | @ -21,7 +21,7 @@ from cloudevents.sdk.converters import base, binary | ||||||
| def test_binary_converter_raise_unsupported(): | def test_binary_converter_raise_unsupported(): | ||||||
|     with pytest.raises(exceptions.UnsupportedEvent): |     with pytest.raises(exceptions.UnsupportedEvent): | ||||||
|         cnvtr = binary.BinaryHTTPCloudEventConverter() |         cnvtr = binary.BinaryHTTPCloudEventConverter() | ||||||
|         cnvtr.read(None, {}, None, None) |         cnvtr.read(None, {}, None, None)  # type: ignore[arg-type] # intentionally wrong type # noqa: E501 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_base_converters_raise_exceptions(): | def test_base_converters_raise_exceptions(): | ||||||
|  | @ -35,8 +35,8 @@ def test_base_converters_raise_exceptions(): | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(Exception): |     with pytest.raises(Exception): | ||||||
|         cnvtr = base.Converter() |         cnvtr = base.Converter() | ||||||
|         cnvtr.write(None, None) |         cnvtr.write(None, None)  # type: ignore[arg-type] # intentionally wrong type | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(Exception): |     with pytest.raises(Exception): | ||||||
|         cnvtr = base.Converter() |         cnvtr = base.Converter() | ||||||
|         cnvtr.read(None, None, None, None) |         cnvtr.read(None, None, None, None)  # type: ignore[arg-type] # intentionally wrong type # noqa: E501 | ||||||
|  |  | ||||||
|  | @ -25,7 +25,7 @@ from cloudevents.tests import data | ||||||
| @pytest.mark.parametrize("event_class", [v03.Event, v1.Event]) | @pytest.mark.parametrize("event_class", [v03.Event, v1.Event]) | ||||||
| def test_binary_converter_upstream(event_class): | def test_binary_converter_upstream(event_class): | ||||||
|     m = marshaller.NewHTTPMarshaller([binary.NewBinaryHTTPCloudEventConverter()]) |     m = marshaller.NewHTTPMarshaller([binary.NewBinaryHTTPCloudEventConverter()]) | ||||||
|     event = m.FromRequest(event_class(), data.headers[event_class], None, lambda x: x) |     event = m.FromRequest(event_class(), data.headers[event_class], b"", lambda x: x) | ||||||
|     assert event is not None |     assert event is not None | ||||||
|     assert event.EventType() == data.ce_type |     assert event.EventType() == data.ce_type | ||||||
|     assert event.EventID() == data.ce_id |     assert event.EventID() == data.ce_id | ||||||
|  |  | ||||||
|  | @ -77,7 +77,7 @@ def test_object_event_v1(): | ||||||
|     _, structured_body = m.ToRequest(event) |     _, structured_body = m.ToRequest(event) | ||||||
|     assert isinstance(structured_body, bytes) |     assert isinstance(structured_body, bytes) | ||||||
|     structured_obj = json.loads(structured_body) |     structured_obj = json.loads(structured_body) | ||||||
|     error_msg = f"Body was {structured_body}, obj is {structured_obj}" |     error_msg = f"Body was {structured_body!r}, obj is {structured_obj}" | ||||||
|     assert isinstance(structured_obj, dict), error_msg |     assert isinstance(structured_obj, dict), error_msg | ||||||
|     assert isinstance(structured_obj["data"], dict), error_msg |     assert isinstance(structured_obj["data"], dict), error_msg | ||||||
|     assert len(structured_obj["data"]) == 1, error_msg |     assert len(structured_obj["data"]) == 1, error_msg | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ | ||||||
| #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||||
| #    License for the specific language governing permissions and limitations | #    License for the specific language governing permissions and limitations | ||||||
| #    under the License. | #    under the License. | ||||||
|  | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
| import bz2 | import bz2 | ||||||
| import io | import io | ||||||
|  | @ -241,11 +242,11 @@ def test_structured_to_request(specversion): | ||||||
|     assert headers["content-type"] == "application/cloudevents+json" |     assert headers["content-type"] == "application/cloudevents+json" | ||||||
|     for key in attributes: |     for key in attributes: | ||||||
|         assert body[key] == attributes[key] |         assert body[key] == attributes[key] | ||||||
|     assert body["data"] == data, f"|{body_bytes}|| {body}" |     assert body["data"] == data, f"|{body_bytes!r}|| {body}" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | ||||||
| def test_attributes_view_accessor(specversion: str): | def test_attributes_view_accessor(specversion: str) -> None: | ||||||
|     attributes: dict[str, typing.Any] = { |     attributes: dict[str, typing.Any] = { | ||||||
|         "specversion": specversion, |         "specversion": specversion, | ||||||
|         "type": "word.found.name", |         "type": "word.found.name", | ||||||
|  | @ -333,7 +334,7 @@ def test_valid_structured_events(specversion): | ||||||
|     events_queue = [] |     events_queue = [] | ||||||
|     num_cloudevents = 30 |     num_cloudevents = 30 | ||||||
|     for i in range(num_cloudevents): |     for i in range(num_cloudevents): | ||||||
|         event = { |         raw_event = { | ||||||
|             "id": f"id{i}", |             "id": f"id{i}", | ||||||
|             "source": f"source{i}.com.test", |             "source": f"source{i}.com.test", | ||||||
|             "type": "cloudevent.test.type", |             "type": "cloudevent.test.type", | ||||||
|  | @ -343,7 +344,7 @@ def test_valid_structured_events(specversion): | ||||||
|         events_queue.append( |         events_queue.append( | ||||||
|             from_http( |             from_http( | ||||||
|                 {"content-type": "application/cloudevents+json"}, |                 {"content-type": "application/cloudevents+json"}, | ||||||
|                 json.dumps(event), |                 json.dumps(raw_event), | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  | @ -454,7 +455,7 @@ def test_invalid_data_format_structured_from_http(): | ||||||
|     headers = {"Content-Type": "application/cloudevents+json"} |     headers = {"Content-Type": "application/cloudevents+json"} | ||||||
|     data = 20 |     data = 20 | ||||||
|     with pytest.raises(cloud_exceptions.InvalidStructuredJSON) as e: |     with pytest.raises(cloud_exceptions.InvalidStructuredJSON) as e: | ||||||
|         from_http(headers, data) |         from_http(headers, data)  # type: ignore[arg-type] # intentionally wrong type | ||||||
|     assert "Expected json of type (str, bytes, bytearray)" in str(e.value) |     assert "Expected json of type (str, bytes, bytearray)" in str(e.value) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -526,7 +527,7 @@ def test_generic_exception(): | ||||||
|     e.errisinstance(cloud_exceptions.MissingRequiredFields) |     e.errisinstance(cloud_exceptions.MissingRequiredFields) | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(cloud_exceptions.GenericException) as e: |     with pytest.raises(cloud_exceptions.GenericException) as e: | ||||||
|         from_http({}, 123) |         from_http({}, 123)  # type: ignore[arg-type] # intentionally wrong type | ||||||
|     e.errisinstance(cloud_exceptions.InvalidStructuredJSON) |     e.errisinstance(cloud_exceptions.InvalidStructuredJSON) | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(cloud_exceptions.GenericException) as e: |     with pytest.raises(cloud_exceptions.GenericException) as e: | ||||||
|  |  | ||||||
|  | @ -19,6 +19,7 @@ import json | ||||||
| import pytest | import pytest | ||||||
| 
 | 
 | ||||||
| from cloudevents import exceptions as cloud_exceptions | from cloudevents import exceptions as cloud_exceptions | ||||||
|  | from cloudevents.abstract.event import AnyCloudEvent | ||||||
| from cloudevents.http import CloudEvent | from cloudevents.http import CloudEvent | ||||||
| from cloudevents.kafka.conversion import ( | from cloudevents.kafka.conversion import ( | ||||||
|     KafkaMessage, |     KafkaMessage, | ||||||
|  | @ -36,7 +37,9 @@ def simple_serialize(data: dict) -> bytes: | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def simple_deserialize(data: bytes) -> dict: | def simple_deserialize(data: bytes) -> dict: | ||||||
|     return json.loads(data.decode()) |     value = json.loads(data.decode()) | ||||||
|  |     assert isinstance(value, dict) | ||||||
|  |     return value | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def failing_func(*args): | def failing_func(*args): | ||||||
|  | @ -47,7 +50,7 @@ class KafkaConversionTestBase: | ||||||
|     expected_data = {"name": "test", "amount": 1} |     expected_data = {"name": "test", "amount": 1} | ||||||
|     expected_custom_mapped_key = "custom-key" |     expected_custom_mapped_key = "custom-key" | ||||||
| 
 | 
 | ||||||
|     def custom_key_mapper(self, _) -> str: |     def custom_key_mapper(self, _: AnyCloudEvent) -> str: | ||||||
|         return self.expected_custom_mapped_key |         return self.expected_custom_mapped_key | ||||||
| 
 | 
 | ||||||
|     @pytest.fixture |     @pytest.fixture | ||||||
|  |  | ||||||
|  | @ -50,14 +50,14 @@ def test_from_request_wrong_unmarshaller(): | ||||||
|     with pytest.raises(exceptions.InvalidDataUnmarshaller): |     with pytest.raises(exceptions.InvalidDataUnmarshaller): | ||||||
|         m = marshaller.NewDefaultHTTPMarshaller() |         m = marshaller.NewDefaultHTTPMarshaller() | ||||||
|         _ = m.FromRequest( |         _ = m.FromRequest( | ||||||
|             event=v1.Event(), headers={}, body="", data_unmarshaller=object() |             event=v1.Event(), headers={}, body="", data_unmarshaller=object()  # type: ignore[arg-type] # intentionally wrong type # noqa: E501 | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_to_request_wrong_marshaller(): | def test_to_request_wrong_marshaller(): | ||||||
|     with pytest.raises(exceptions.InvalidDataMarshaller): |     with pytest.raises(exceptions.InvalidDataMarshaller): | ||||||
|         m = marshaller.NewDefaultHTTPMarshaller() |         m = marshaller.NewDefaultHTTPMarshaller() | ||||||
|         _ = m.ToRequest(v1.Event(), data_marshaller="") |         _ = m.ToRequest(v1.Event(), data_marshaller="")  # type: ignore[arg-type] # intentionally wrong type # noqa: E501 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_from_request_cannot_read(binary_headers): | def test_from_request_cannot_read(binary_headers): | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ | ||||||
| #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||||
| #    License for the specific language governing permissions and limitations | #    License for the specific language governing permissions and limitations | ||||||
| #    under the License. | #    under the License. | ||||||
|  | from __future__ import annotations | ||||||
| 
 | 
 | ||||||
| import bz2 | import bz2 | ||||||
| import io | import io | ||||||
|  | @ -28,10 +29,13 @@ from cloudevents.pydantic.v1.conversion import from_http as pydantic_v1_from_htt | ||||||
| from cloudevents.pydantic.v1.event import CloudEvent as PydanticV1CloudEvent | from cloudevents.pydantic.v1.event import CloudEvent as PydanticV1CloudEvent | ||||||
| from cloudevents.pydantic.v2.conversion import from_http as pydantic_v2_from_http | from cloudevents.pydantic.v2.conversion import from_http as pydantic_v2_from_http | ||||||
| from cloudevents.pydantic.v2.event import CloudEvent as PydanticV2CloudEvent | from cloudevents.pydantic.v2.event import CloudEvent as PydanticV2CloudEvent | ||||||
| from cloudevents.sdk import converters | from cloudevents.sdk import converters, types | ||||||
| from cloudevents.sdk.converters.binary import is_binary | from cloudevents.sdk.converters.binary import is_binary | ||||||
| from cloudevents.sdk.converters.structured import is_structured | from cloudevents.sdk.converters.structured import is_structured | ||||||
| 
 | 
 | ||||||
|  | if typing.TYPE_CHECKING: | ||||||
|  |     from typing_extensions import TypeAlias | ||||||
|  | 
 | ||||||
| invalid_test_headers = [ | invalid_test_headers = [ | ||||||
|     { |     { | ||||||
|         "ce-source": "<event-source>", |         "ce-source": "<event-source>", | ||||||
|  | @ -70,7 +74,30 @@ test_data = {"payload-content": "Hello World!"} | ||||||
| 
 | 
 | ||||||
| app = Sanic("test_pydantic_http_events") | app = Sanic("test_pydantic_http_events") | ||||||
| 
 | 
 | ||||||
| _pydantic_implementation = { | 
 | ||||||
|  | AnyPydanticCloudEvent: TypeAlias = typing.Union[ | ||||||
|  |     PydanticV1CloudEvent, PydanticV2CloudEvent | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class FromHttpFn(typing.Protocol): | ||||||
|  |     def __call__( | ||||||
|  |         self, | ||||||
|  |         headers: typing.Dict[str, str], | ||||||
|  |         data: typing.Optional[typing.AnyStr], | ||||||
|  |         data_unmarshaller: typing.Optional[types.UnmarshallerType] = None, | ||||||
|  |     ) -> AnyPydanticCloudEvent: | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PydanticImplementation(typing.TypedDict): | ||||||
|  |     event: typing.Type[AnyPydanticCloudEvent] | ||||||
|  |     validation_error: typing.Type[Exception] | ||||||
|  |     from_http: FromHttpFn | ||||||
|  |     pydantic_version: typing.Literal["v1", "v2"] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | _pydantic_implementation: typing.Mapping[str, PydanticImplementation] = { | ||||||
|     "v1": { |     "v1": { | ||||||
|         "event": PydanticV1CloudEvent, |         "event": PydanticV1CloudEvent, | ||||||
|         "validation_error": PydanticV1ValidationError, |         "validation_error": PydanticV1ValidationError, | ||||||
|  | @ -87,7 +114,9 @@ _pydantic_implementation = { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.fixture(params=["v1", "v2"]) | @pytest.fixture(params=["v1", "v2"]) | ||||||
| def cloudevents_implementation(request): | def cloudevents_implementation( | ||||||
|  |     request: pytest.FixtureRequest, | ||||||
|  | ) -> PydanticImplementation: | ||||||
|     return _pydantic_implementation[request.param] |     return _pydantic_implementation[request.param] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -108,7 +137,9 @@ async def echo(request, pydantic_version): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("body", invalid_cloudevent_request_body) | @pytest.mark.parametrize("body", invalid_cloudevent_request_body) | ||||||
| def test_missing_required_fields_structured(body, cloudevents_implementation): | def test_missing_required_fields_structured( | ||||||
|  |     body: dict, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     with pytest.raises(cloud_exceptions.MissingRequiredFields): |     with pytest.raises(cloud_exceptions.MissingRequiredFields): | ||||||
|         _ = cloudevents_implementation["from_http"]( |         _ = cloudevents_implementation["from_http"]( | ||||||
|             {"Content-Type": "application/cloudevents+json"}, json.dumps(body) |             {"Content-Type": "application/cloudevents+json"}, json.dumps(body) | ||||||
|  | @ -116,20 +147,26 @@ def test_missing_required_fields_structured(body, cloudevents_implementation): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("headers", invalid_test_headers) | @pytest.mark.parametrize("headers", invalid_test_headers) | ||||||
| def test_missing_required_fields_binary(headers, cloudevents_implementation): | def test_missing_required_fields_binary( | ||||||
|  |     headers: dict, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     with pytest.raises(cloud_exceptions.MissingRequiredFields): |     with pytest.raises(cloud_exceptions.MissingRequiredFields): | ||||||
|         _ = cloudevents_implementation["from_http"](headers, json.dumps(test_data)) |         _ = cloudevents_implementation["from_http"](headers, json.dumps(test_data)) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("headers", invalid_test_headers) | @pytest.mark.parametrize("headers", invalid_test_headers) | ||||||
| def test_missing_required_fields_empty_data_binary(headers, cloudevents_implementation): | def test_missing_required_fields_empty_data_binary( | ||||||
|  |     headers: dict, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     # Test for issue #115 |     # Test for issue #115 | ||||||
|     with pytest.raises(cloud_exceptions.MissingRequiredFields): |     with pytest.raises(cloud_exceptions.MissingRequiredFields): | ||||||
|         _ = cloudevents_implementation["from_http"](headers, None) |         _ = cloudevents_implementation["from_http"](headers, None) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | ||||||
| def test_emit_binary_event(specversion, cloudevents_implementation): | def test_emit_binary_event( | ||||||
|  |     specversion: str, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     headers = { |     headers = { | ||||||
|         "ce-id": "my-id", |         "ce-id": "my-id", | ||||||
|         "ce-source": "<event-source>", |         "ce-source": "<event-source>", | ||||||
|  | @ -159,7 +196,9 @@ def test_emit_binary_event(specversion, cloudevents_implementation): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | ||||||
| def test_emit_structured_event(specversion, cloudevents_implementation): | def test_emit_structured_event( | ||||||
|  |     specversion: str, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     headers = {"Content-Type": "application/cloudevents+json"} |     headers = {"Content-Type": "application/cloudevents+json"} | ||||||
|     body = { |     body = { | ||||||
|         "id": "my-id", |         "id": "my-id", | ||||||
|  | @ -188,7 +227,11 @@ def test_emit_structured_event(specversion, cloudevents_implementation): | ||||||
|     "converter", [converters.TypeBinary, converters.TypeStructured] |     "converter", [converters.TypeBinary, converters.TypeStructured] | ||||||
| ) | ) | ||||||
| @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | ||||||
| def test_roundtrip_non_json_event(converter, specversion, cloudevents_implementation): | def test_roundtrip_non_json_event( | ||||||
|  |     converter: str, | ||||||
|  |     specversion: str, | ||||||
|  |     cloudevents_implementation: PydanticImplementation, | ||||||
|  | ) -> None: | ||||||
|     input_data = io.BytesIO() |     input_data = io.BytesIO() | ||||||
|     for _ in range(100): |     for _ in range(100): | ||||||
|         for j in range(20): |         for j in range(20): | ||||||
|  | @ -217,7 +260,9 @@ def test_roundtrip_non_json_event(converter, specversion, cloudevents_implementa | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | ||||||
| def test_missing_ce_prefix_binary_event(specversion, cloudevents_implementation): | def test_missing_ce_prefix_binary_event( | ||||||
|  |     specversion: str, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     prefixed_headers = {} |     prefixed_headers = {} | ||||||
|     headers = { |     headers = { | ||||||
|         "ce-id": "my-id", |         "ce-id": "my-id", | ||||||
|  | @ -240,9 +285,11 @@ def test_missing_ce_prefix_binary_event(specversion, cloudevents_implementation) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | ||||||
| def test_valid_binary_events(specversion, cloudevents_implementation): | def test_valid_binary_events( | ||||||
|  |     specversion: str, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     # Test creating multiple cloud events |     # Test creating multiple cloud events | ||||||
|     events_queue = [] |     events_queue: list[AnyPydanticCloudEvent] = [] | ||||||
|     headers = {} |     headers = {} | ||||||
|     num_cloudevents = 30 |     num_cloudevents = 30 | ||||||
|     for i in range(num_cloudevents): |     for i in range(num_cloudevents): | ||||||
|  | @ -258,7 +305,7 @@ def test_valid_binary_events(specversion, cloudevents_implementation): | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     for i, event in enumerate(events_queue): |     for i, event in enumerate(events_queue): | ||||||
|         data = event.data |         assert isinstance(event.data, dict) | ||||||
|         assert event["id"] == f"id{i}" |         assert event["id"] == f"id{i}" | ||||||
|         assert event["source"] == f"source{i}.com.test" |         assert event["source"] == f"source{i}.com.test" | ||||||
|         assert event["specversion"] == specversion |         assert event["specversion"] == specversion | ||||||
|  | @ -266,7 +313,9 @@ def test_valid_binary_events(specversion, cloudevents_implementation): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | ||||||
| def test_structured_to_request(specversion, cloudevents_implementation): | def test_structured_to_request( | ||||||
|  |     specversion: str, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     attributes = { |     attributes = { | ||||||
|         "specversion": specversion, |         "specversion": specversion, | ||||||
|         "type": "word.found.name", |         "type": "word.found.name", | ||||||
|  | @ -283,11 +332,13 @@ def test_structured_to_request(specversion, cloudevents_implementation): | ||||||
|     assert headers["content-type"] == "application/cloudevents+json" |     assert headers["content-type"] == "application/cloudevents+json" | ||||||
|     for key in attributes: |     for key in attributes: | ||||||
|         assert body[key] == attributes[key] |         assert body[key] == attributes[key] | ||||||
|     assert body["data"] == data, f"|{body_bytes}|| {body}" |     assert body["data"] == data, f"|{body_bytes!r}|| {body}" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | ||||||
| def test_attributes_view_accessor(specversion: str, cloudevents_implementation): | def test_attributes_view_accessor( | ||||||
|  |     specversion: str, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     attributes: dict[str, typing.Any] = { |     attributes: dict[str, typing.Any] = { | ||||||
|         "specversion": specversion, |         "specversion": specversion, | ||||||
|         "type": "word.found.name", |         "type": "word.found.name", | ||||||
|  | @ -296,9 +347,7 @@ def test_attributes_view_accessor(specversion: str, cloudevents_implementation): | ||||||
|     } |     } | ||||||
|     data = {"message": "Hello World!"} |     data = {"message": "Hello World!"} | ||||||
| 
 | 
 | ||||||
|     event: cloudevents_implementation["event"] = cloudevents_implementation["event"]( |     event = cloudevents_implementation["event"](attributes, data) | ||||||
|         attributes, data |  | ||||||
|     ) |  | ||||||
|     event_attributes: typing.Mapping[str, typing.Any] = event.get_attributes() |     event_attributes: typing.Mapping[str, typing.Any] = event.get_attributes() | ||||||
|     assert event_attributes["specversion"] == attributes["specversion"] |     assert event_attributes["specversion"] == attributes["specversion"] | ||||||
|     assert event_attributes["type"] == attributes["type"] |     assert event_attributes["type"] == attributes["type"] | ||||||
|  | @ -308,7 +357,9 @@ def test_attributes_view_accessor(specversion: str, cloudevents_implementation): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | ||||||
| def test_binary_to_request(specversion, cloudevents_implementation): | def test_binary_to_request( | ||||||
|  |     specversion: str, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     attributes = { |     attributes = { | ||||||
|         "specversion": specversion, |         "specversion": specversion, | ||||||
|         "type": "word.found.name", |         "type": "word.found.name", | ||||||
|  | @ -327,7 +378,9 @@ def test_binary_to_request(specversion, cloudevents_implementation): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | ||||||
| def test_empty_data_structured_event(specversion, cloudevents_implementation): | def test_empty_data_structured_event( | ||||||
|  |     specversion: str, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     # Testing if cloudevent breaks when no structured data field present |     # Testing if cloudevent breaks when no structured data field present | ||||||
|     attributes = { |     attributes = { | ||||||
|         "specversion": specversion, |         "specversion": specversion, | ||||||
|  | @ -352,7 +405,9 @@ def test_empty_data_structured_event(specversion, cloudevents_implementation): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | ||||||
| def test_empty_data_binary_event(specversion, cloudevents_implementation): | def test_empty_data_binary_event( | ||||||
|  |     specversion: str, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     # Testing if cloudevent breaks when no structured data field present |     # Testing if cloudevent breaks when no structured data field present | ||||||
|     headers = { |     headers = { | ||||||
|         "Content-Type": "application/octet-stream", |         "Content-Type": "application/octet-stream", | ||||||
|  | @ -372,12 +427,14 @@ def test_empty_data_binary_event(specversion, cloudevents_implementation): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | ||||||
| def test_valid_structured_events(specversion, cloudevents_implementation): | def test_valid_structured_events( | ||||||
|  |     specversion: str, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     # Test creating multiple cloud events |     # Test creating multiple cloud events | ||||||
|     events_queue = [] |     events_queue: list[AnyPydanticCloudEvent] = [] | ||||||
|     num_cloudevents = 30 |     num_cloudevents = 30 | ||||||
|     for i in range(num_cloudevents): |     for i in range(num_cloudevents): | ||||||
|         event = { |         raw_event = { | ||||||
|             "id": f"id{i}", |             "id": f"id{i}", | ||||||
|             "source": f"source{i}.com.test", |             "source": f"source{i}.com.test", | ||||||
|             "type": "cloudevent.test.type", |             "type": "cloudevent.test.type", | ||||||
|  | @ -387,11 +444,12 @@ def test_valid_structured_events(specversion, cloudevents_implementation): | ||||||
|         events_queue.append( |         events_queue.append( | ||||||
|             cloudevents_implementation["from_http"]( |             cloudevents_implementation["from_http"]( | ||||||
|                 {"content-type": "application/cloudevents+json"}, |                 {"content-type": "application/cloudevents+json"}, | ||||||
|                 json.dumps(event), |                 json.dumps(raw_event), | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     for i, event in enumerate(events_queue): |     for i, event in enumerate(events_queue): | ||||||
|  |         assert isinstance(event.data, dict) | ||||||
|         assert event["id"] == f"id{i}" |         assert event["id"] == f"id{i}" | ||||||
|         assert event["source"] == f"source{i}.com.test" |         assert event["source"] == f"source{i}.com.test" | ||||||
|         assert event["specversion"] == specversion |         assert event["specversion"] == specversion | ||||||
|  | @ -399,7 +457,9 @@ def test_valid_structured_events(specversion, cloudevents_implementation): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | ||||||
| def test_structured_no_content_type(specversion, cloudevents_implementation): | def test_structured_no_content_type( | ||||||
|  |     specversion: str, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     # Test creating multiple cloud events |     # Test creating multiple cloud events | ||||||
|     data = { |     data = { | ||||||
|         "id": "id", |         "id": "id", | ||||||
|  | @ -410,6 +470,7 @@ def test_structured_no_content_type(specversion, cloudevents_implementation): | ||||||
|     } |     } | ||||||
|     event = cloudevents_implementation["from_http"]({}, json.dumps(data)) |     event = cloudevents_implementation["from_http"]({}, json.dumps(data)) | ||||||
| 
 | 
 | ||||||
|  |     assert isinstance(event.data, dict) | ||||||
|     assert event["id"] == "id" |     assert event["id"] == "id" | ||||||
|     assert event["source"] == "source.com.test" |     assert event["source"] == "source.com.test" | ||||||
|     assert event["specversion"] == specversion |     assert event["specversion"] == specversion | ||||||
|  | @ -437,7 +498,9 @@ def test_is_binary(): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | ||||||
| def test_cloudevent_repr(specversion, cloudevents_implementation): | def test_cloudevent_repr( | ||||||
|  |     specversion: str, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     headers = { |     headers = { | ||||||
|         "Content-Type": "application/octet-stream", |         "Content-Type": "application/octet-stream", | ||||||
|         "ce-specversion": specversion, |         "ce-specversion": specversion, | ||||||
|  | @ -454,7 +517,9 @@ def test_cloudevent_repr(specversion, cloudevents_implementation): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | @pytest.mark.parametrize("specversion", ["1.0", "0.3"]) | ||||||
| def test_none_data_cloudevent(specversion, cloudevents_implementation): | def test_none_data_cloudevent( | ||||||
|  |     specversion: str, cloudevents_implementation: PydanticImplementation | ||||||
|  | ) -> None: | ||||||
|     event = cloudevents_implementation["event"]( |     event = cloudevents_implementation["event"]( | ||||||
|         { |         { | ||||||
|             "source": "<my-url>", |             "source": "<my-url>", | ||||||
|  | @ -466,7 +531,7 @@ def test_none_data_cloudevent(specversion, cloudevents_implementation): | ||||||
|     to_structured(event) |     to_structured(event) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_wrong_specversion(cloudevents_implementation): | def test_wrong_specversion(cloudevents_implementation: PydanticImplementation) -> None: | ||||||
|     headers = {"Content-Type": "application/cloudevents+json"} |     headers = {"Content-Type": "application/cloudevents+json"} | ||||||
|     data = json.dumps( |     data = json.dumps( | ||||||
|         { |         { | ||||||
|  | @ -481,15 +546,19 @@ def test_wrong_specversion(cloudevents_implementation): | ||||||
|     assert "Found invalid specversion 0.2" in str(e.value) |     assert "Found invalid specversion 0.2" in str(e.value) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_invalid_data_format_structured_from_http(cloudevents_implementation): | def test_invalid_data_format_structured_from_http( | ||||||
|  |     cloudevents_implementation: PydanticImplementation, | ||||||
|  | ) -> None: | ||||||
|     headers = {"Content-Type": "application/cloudevents+json"} |     headers = {"Content-Type": "application/cloudevents+json"} | ||||||
|     data = 20 |     data = 20 | ||||||
|     with pytest.raises(cloud_exceptions.InvalidStructuredJSON) as e: |     with pytest.raises(cloud_exceptions.InvalidStructuredJSON) as e: | ||||||
|         cloudevents_implementation["from_http"](headers, data) |         cloudevents_implementation["from_http"](headers, data)  # type: ignore[type-var] # intentionally wrong type # noqa: E501 | ||||||
|     assert "Expected json of type (str, bytes, bytearray)" in str(e.value) |     assert "Expected json of type (str, bytes, bytearray)" in str(e.value) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_wrong_specversion_to_request(cloudevents_implementation): | def test_wrong_specversion_to_request( | ||||||
|  |     cloudevents_implementation: PydanticImplementation, | ||||||
|  | ) -> None: | ||||||
|     event = cloudevents_implementation["event"]({"source": "s", "type": "t"}, None) |     event = cloudevents_implementation["event"]({"source": "s", "type": "t"}, None) | ||||||
|     with pytest.raises(cloud_exceptions.InvalidRequiredFields) as e: |     with pytest.raises(cloud_exceptions.InvalidRequiredFields) as e: | ||||||
|         event["specversion"] = "0.2" |         event["specversion"] = "0.2" | ||||||
|  | @ -513,7 +582,9 @@ def test_is_structured(): | ||||||
|     assert not is_structured(headers) |     assert not is_structured(headers) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_empty_json_structured(cloudevents_implementation): | def test_empty_json_structured( | ||||||
|  |     cloudevents_implementation: PydanticImplementation, | ||||||
|  | ) -> None: | ||||||
|     headers = {"Content-Type": "application/cloudevents+json"} |     headers = {"Content-Type": "application/cloudevents+json"} | ||||||
|     data = "" |     data = "" | ||||||
|     with pytest.raises(cloud_exceptions.MissingRequiredFields) as e: |     with pytest.raises(cloud_exceptions.MissingRequiredFields) as e: | ||||||
|  | @ -521,7 +592,9 @@ def test_empty_json_structured(cloudevents_implementation): | ||||||
|     assert "Failed to read specversion from both headers and data" in str(e.value) |     assert "Failed to read specversion from both headers and data" in str(e.value) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_uppercase_headers_with_none_data_binary(cloudevents_implementation): | def test_uppercase_headers_with_none_data_binary( | ||||||
|  |     cloudevents_implementation: PydanticImplementation, | ||||||
|  | ) -> None: | ||||||
|     headers = { |     headers = { | ||||||
|         "Ce-Id": "my-id", |         "Ce-Id": "my-id", | ||||||
|         "Ce-Source": "<event-source>", |         "Ce-Source": "<event-source>", | ||||||
|  | @ -538,7 +611,7 @@ def test_uppercase_headers_with_none_data_binary(cloudevents_implementation): | ||||||
|     assert new_data is None |     assert new_data is None | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_generic_exception(cloudevents_implementation): | def test_generic_exception(cloudevents_implementation: PydanticImplementation) -> None: | ||||||
|     headers = {"Content-Type": "application/cloudevents+json"} |     headers = {"Content-Type": "application/cloudevents+json"} | ||||||
|     data = json.dumps( |     data = json.dumps( | ||||||
|         { |         { | ||||||
|  | @ -554,7 +627,7 @@ def test_generic_exception(cloudevents_implementation): | ||||||
|     e.errisinstance(cloud_exceptions.MissingRequiredFields) |     e.errisinstance(cloud_exceptions.MissingRequiredFields) | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(cloud_exceptions.GenericException) as e: |     with pytest.raises(cloud_exceptions.GenericException) as e: | ||||||
|         cloudevents_implementation["from_http"]({}, 123) |         cloudevents_implementation["from_http"]({}, 123)  # type: ignore[type-var] # intentionally wrong type # noqa: E501 | ||||||
|     e.errisinstance(cloud_exceptions.InvalidStructuredJSON) |     e.errisinstance(cloud_exceptions.InvalidStructuredJSON) | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(cloud_exceptions.GenericException) as e: |     with pytest.raises(cloud_exceptions.GenericException) as e: | ||||||
|  | @ -569,7 +642,9 @@ def test_generic_exception(cloudevents_implementation): | ||||||
|     e.errisinstance(cloud_exceptions.DataMarshallerError) |     e.errisinstance(cloud_exceptions.DataMarshallerError) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_non_dict_data_no_headers_bug(cloudevents_implementation): | def test_non_dict_data_no_headers_bug( | ||||||
|  |     cloudevents_implementation: PydanticImplementation, | ||||||
|  | ) -> None: | ||||||
|     # Test for issue #116 |     # Test for issue #116 | ||||||
|     headers = {"Content-Type": "application/cloudevents+json"} |     headers = {"Content-Type": "application/cloudevents+json"} | ||||||
|     data = "123" |     data = "123" | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								mypy.ini
								
								
								
								
							
							
						
						
									
										2
									
								
								mypy.ini
								
								
								
								
							|  | @ -1,6 +1,6 @@ | ||||||
| [mypy] | [mypy] | ||||||
| plugins = pydantic.mypy | plugins = pydantic.mypy | ||||||
| python_version = 3.8 | python_version = 3.9 | ||||||
| 
 | 
 | ||||||
| pretty = True | pretty = True | ||||||
| show_error_context = True | show_error_context = True | ||||||
|  |  | ||||||
|  | @ -5,3 +5,4 @@ pep8-naming | ||||||
| flake8-print | flake8-print | ||||||
| tox | tox | ||||||
| pre-commit | pre-commit | ||||||
|  | mypy | ||||||
|  |  | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | mypy | ||||||
|  | # mypy has the pydantic plugin enabled | ||||||
|  | pydantic>=2.0.0,<3.0 | ||||||
|  | types-requests | ||||||
|  | deprecation>=2.0,<3.0 | ||||||
|  | @ -25,7 +25,7 @@ resp = requests.get( | ||||||
| image_bytes = resp.content | image_bytes = resp.content | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def send_binary_cloud_event(url: str): | def send_binary_cloud_event(url: str) -> None: | ||||||
|     # Create cloudevent |     # Create cloudevent | ||||||
|     attributes = { |     attributes = { | ||||||
|         "type": "com.example.string", |         "type": "com.example.string", | ||||||
|  | @ -42,7 +42,7 @@ def send_binary_cloud_event(url: str): | ||||||
|     print(f"Sent {event['id']} of type {event['type']}") |     print(f"Sent {event['id']} of type {event['type']}") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def send_structured_cloud_event(url: str): | def send_structured_cloud_event(url: str) -> None: | ||||||
|     # Create cloudevent |     # Create cloudevent | ||||||
|     attributes = { |     attributes = { | ||||||
|         "type": "com.example.base64", |         "type": "com.example.base64", | ||||||
|  |  | ||||||
							
								
								
									
										1
									
								
								setup.py
								
								
								
								
							
							
						
						
									
										1
									
								
								setup.py
								
								
								
								
							|  | @ -65,7 +65,6 @@ if __name__ == "__main__": | ||||||
|             "Programming Language :: Python", |             "Programming Language :: Python", | ||||||
|             "Programming Language :: Python :: 3", |             "Programming Language :: Python :: 3", | ||||||
|             "Programming Language :: Python :: 3 :: Only", |             "Programming Language :: Python :: 3 :: Only", | ||||||
|             "Programming Language :: Python :: 3.8", |  | ||||||
|             "Programming Language :: Python :: 3.9", |             "Programming Language :: Python :: 3.9", | ||||||
|             "Programming Language :: Python :: 3.10", |             "Programming Language :: Python :: 3.10", | ||||||
|             "Programming Language :: Python :: 3.11", |             "Programming Language :: Python :: 3.11", | ||||||
|  |  | ||||||
							
								
								
									
										20
									
								
								tox.ini
								
								
								
								
							
							
						
						
									
										20
									
								
								tox.ini
								
								
								
								
							|  | @ -1,5 +1,5 @@ | ||||||
| [tox] | [tox] | ||||||
| envlist = py{38,39,310,311,312},lint | envlist = py{38,39,310,311,312},lint,mypy,mypy-samples-{image,json} | ||||||
| skipsdist = True | skipsdist = True | ||||||
| 
 | 
 | ||||||
| [testenv] | [testenv] | ||||||
|  | @ -30,3 +30,21 @@ commands = | ||||||
|     black --check . |     black --check . | ||||||
|     isort -c cloudevents samples |     isort -c cloudevents samples | ||||||
|     flake8 cloudevents samples --ignore W503,E731 --extend-ignore E203 --max-line-length 88 |     flake8 cloudevents samples --ignore W503,E731 --extend-ignore E203 --max-line-length 88 | ||||||
|  | 
 | ||||||
|  | [testenv:mypy] | ||||||
|  | basepython = python3.11 | ||||||
|  | deps = | ||||||
|  |     -r{toxinidir}/requirements/mypy.txt | ||||||
|  |     # mypy needs test dependencies to check test modules | ||||||
|  |     -r{toxinidir}/requirements/test.txt | ||||||
|  | commands = mypy cloudevents | ||||||
|  | 
 | ||||||
|  | [testenv:mypy-samples-{image,json}] | ||||||
|  | basepython = python3.11 | ||||||
|  | setenv = | ||||||
|  |     mypy-samples-image: SAMPLE_DIR={toxinidir}/samples/http-image-cloudevents | ||||||
|  |     mypy-samples-json: SAMPLE_DIR={toxinidir}/samples/http-json-cloudevents | ||||||
|  | deps = | ||||||
|  |     -r{toxinidir}/requirements/mypy.txt | ||||||
|  |     -r{env:SAMPLE_DIR}/requirements.txt | ||||||
|  | commands = mypy {env:SAMPLE_DIR} | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue