chore: Improve exceptions and introduce a new one for invalid values

Signed-off-by: Tudor Plugaru <plugaru.tudor@protonmail.com>
This commit is contained in:
Tudor Plugaru 2024-11-14 10:47:17 +02:00
parent 1d43d688be
commit 21493e180f
No known key found for this signature in database
3 changed files with 97 additions and 106 deletions

View File

@ -22,6 +22,7 @@ from cloudevents.core.v1.exceptions import (
CloudEventValidationError, CloudEventValidationError,
CustomExtensionAttributeError, CustomExtensionAttributeError,
InvalidAttributeTypeError, InvalidAttributeTypeError,
InvalidAttributeValueError,
MissingRequiredAttributeError, MissingRequiredAttributeError,
) )
@ -84,41 +85,37 @@ class CloudEvent:
errors = defaultdict(list) errors = defaultdict(list)
if "id" not in attributes: if "id" not in attributes:
errors["id"].append(MissingRequiredAttributeError(missing="id")) errors["id"].append(MissingRequiredAttributeError(attribute_name="id"))
if attributes.get("id") is None: if attributes.get("id") is None:
errors["id"].append( errors["id"].append(
InvalidAttributeTypeError("Attribute 'id' must not be None") InvalidAttributeValueError("id", "Attribute 'id' must not be None")
) )
if not isinstance(attributes.get("id"), str): if not isinstance(attributes.get("id"), str):
errors["id"].append( errors["id"].append(InvalidAttributeTypeError("id", str))
InvalidAttributeTypeError("Attribute 'id' must be a string")
)
if "source" not in attributes: if "source" not in attributes:
errors["source"].append(MissingRequiredAttributeError(missing="source"))
if not isinstance(attributes.get("source"), str):
errors["source"].append( errors["source"].append(
InvalidAttributeTypeError("Attribute 'source' must be a string") MissingRequiredAttributeError(attribute_name="source")
) )
if not isinstance(attributes.get("source"), str):
errors["source"].append(InvalidAttributeTypeError("source", str))
if "type" not in attributes: if "type" not in attributes:
errors["type"].append(MissingRequiredAttributeError(missing="type")) errors["type"].append(MissingRequiredAttributeError(attribute_name="type"))
if not isinstance(attributes.get("type"), str): if not isinstance(attributes.get("type"), str):
errors["type"].append( errors["type"].append(InvalidAttributeTypeError("type", str))
InvalidAttributeTypeError("Attribute 'type' must be a string")
)
if "specversion" not in attributes: if "specversion" not in attributes:
errors["specversion"].append( errors["specversion"].append(
MissingRequiredAttributeError(missing="specversion") MissingRequiredAttributeError(attribute_name="specversion")
) )
if not isinstance(attributes.get("specversion"), str): if not isinstance(attributes.get("specversion"), str):
errors["specversion"].append( errors["specversion"].append(InvalidAttributeTypeError("specversion", str))
InvalidAttributeTypeError("Attribute 'specversion' must be a string")
)
if attributes.get("specversion") != "1.0": if attributes.get("specversion") != "1.0":
errors["specversion"].append( errors["specversion"].append(
InvalidAttributeTypeError("Attribute 'specversion' must be '1.0'") InvalidAttributeValueError(
"specversion", "Attribute 'specversion' must be '1.0'"
)
) )
return errors return errors
@ -136,46 +133,43 @@ class CloudEvent:
if "time" in attributes: if "time" in attributes:
if not isinstance(attributes["time"], datetime): if not isinstance(attributes["time"], datetime):
errors["time"].append( errors["time"].append(InvalidAttributeTypeError("time", datetime))
InvalidAttributeTypeError(
"Attribute 'time' must be a datetime object"
)
)
if hasattr(attributes["time"], "tzinfo") and not attributes["time"].tzinfo: if hasattr(attributes["time"], "tzinfo") and not attributes["time"].tzinfo:
errors["time"].append( errors["time"].append(
InvalidAttributeTypeError("Attribute 'time' must be timezone aware") InvalidAttributeValueError(
"time", "Attribute 'time' must be timezone aware"
)
) )
if "subject" in attributes: if "subject" in attributes:
if not isinstance(attributes["subject"], str): if not isinstance(attributes["subject"], str):
errors["subject"].append( errors["subject"].append(InvalidAttributeTypeError("subject", str))
InvalidAttributeTypeError("Attribute 'subject' must be a string")
)
if not attributes["subject"]: if not attributes["subject"]:
errors["subject"].append( errors["subject"].append(
InvalidAttributeTypeError("Attribute 'subject' must not be empty") InvalidAttributeValueError(
"subject", "Attribute 'subject' must not be empty"
)
) )
if "datacontenttype" in attributes: if "datacontenttype" in attributes:
if not isinstance(attributes["datacontenttype"], str): if not isinstance(attributes["datacontenttype"], str):
errors["datacontenttype"].append( errors["datacontenttype"].append(
InvalidAttributeTypeError( InvalidAttributeTypeError("datacontenttype", str)
"Attribute 'datacontenttype' must be a string"
)
) )
if not attributes["datacontenttype"]: if not attributes["datacontenttype"]:
errors["datacontenttype"].append( errors["datacontenttype"].append(
InvalidAttributeTypeError( InvalidAttributeValueError(
"Attribute 'datacontenttype' must not be empty" "datacontenttype",
"Attribute 'datacontenttype' must not be empty",
) )
) )
if "dataschema" in attributes: if "dataschema" in attributes:
if not isinstance(attributes["dataschema"], str): if not isinstance(attributes["dataschema"], str):
errors["dataschema"].append( errors["dataschema"].append(
InvalidAttributeTypeError("Attribute 'dataschema' must be a string") InvalidAttributeTypeError("dataschema", str)
) )
if not attributes["dataschema"]: if not attributes["dataschema"]:
errors["dataschema"].append( errors["dataschema"].append(
InvalidAttributeTypeError( InvalidAttributeValueError(
"Attribute 'dataschema' must not be empty" "dataschema", "Attribute 'dataschema' must not be empty"
) )
) )
return errors return errors
@ -200,19 +194,22 @@ class CloudEvent:
if extension_attribute == "data": if extension_attribute == "data":
errors[extension_attribute].append( errors[extension_attribute].append(
CustomExtensionAttributeError( CustomExtensionAttributeError(
"Extension attribute 'data' is reserved and must not be used" extension_attribute,
"Extension attribute 'data' is reserved and must not be used",
) )
) )
if not (1 <= len(extension_attribute) <= 20): if not (1 <= len(extension_attribute) <= 20):
errors[extension_attribute].append( errors[extension_attribute].append(
CustomExtensionAttributeError( CustomExtensionAttributeError(
f"Extension attribute '{extension_attribute}' should be between 1 and 20 characters long" extension_attribute,
f"Extension attribute '{extension_attribute}' should be between 1 and 20 characters long",
) )
) )
if not re.match(r"^[a-z0-9]+$", extension_attribute): if not re.match(r"^[a-z0-9]+$", extension_attribute):
errors[extension_attribute].append( errors[extension_attribute].append(
CustomExtensionAttributeError( CustomExtensionAttributeError(
f"Extension attribute '{extension_attribute}' should only contain lowercase letters and numbers" extension_attribute,
f"Extension attribute '{extension_attribute}' should only contain lowercase letters and numbers",
) )
) )
return errors return errors

View File

@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
class BaseCloudEventException(Exception): class BaseCloudEventException(Exception):
"""A CloudEvent generic exception."""
pass pass
@ -25,7 +27,7 @@ class CloudEventValidationError(BaseCloudEventException):
:param errors: The errors gathered during the CloudEvent creation where key :param errors: The errors gathered during the CloudEvent creation where key
is the name of the attribute and value is a list of errors related to that attribute. is the name of the attribute and value is a list of errors related to that attribute.
""" """
super().__init__("Validation errors occurred") super().__init__("Failed to create CloudEvent due to the validation errors:")
self.errors: dict[str, list[BaseCloudEventException]] = errors self.errors: dict[str, list[BaseCloudEventException]] = errors
def __str__(self) -> str: def __str__(self) -> str:
@ -35,22 +37,40 @@ class CloudEventValidationError(BaseCloudEventException):
return f"{super().__str__()}: {', '.join(error_messages)}" return f"{super().__str__()}: {', '.join(error_messages)}"
class MissingRequiredAttributeError(BaseCloudEventException): class MissingRequiredAttributeError(BaseCloudEventException, ValueError):
""" """
Exception for missing required attribute. Raised for attributes that are required to be present by the specification.
""" """
def __init__(self, missing: str) -> None: def __init__(self, attribute_name: str) -> None:
super().__init__(f"Missing required attribute: '{missing}'") super().__init__(f"Missing required attribute: '{attribute_name}'")
class CustomExtensionAttributeError(BaseCloudEventException): class CustomExtensionAttributeError(BaseCloudEventException, ValueError):
""" """
Exception for invalid custom extension names. Raised when a custom extension attribute violates naming conventions.
""" """
def __init__(self, extension_attribute: str, msg: str) -> None:
self.extension_attribute = extension_attribute
super().__init__(msg)
class InvalidAttributeTypeError(BaseCloudEventException):
class InvalidAttributeTypeError(BaseCloudEventException, TypeError):
""" """
Exception for invalid attribute type. Raised when an attribute has an unsupported type.
""" """
def __init__(self, attribute_name: str, expected_type: type) -> None:
self.attribute_name = attribute_name
super().__init__(f"Attribute '{attribute_name}' must be a {expected_type}")
class InvalidAttributeValueError(BaseCloudEventException, ValueError):
"""
Raised when an attribute has an invalid value.
"""
def __init__(self, attribute_name: str, msg: str) -> None:
self.attribute_name = attribute_name
super().__init__(msg)

View File

@ -22,6 +22,7 @@ from cloudevents.core.v1.exceptions import (
CloudEventValidationError, CloudEventValidationError,
CustomExtensionAttributeError, CustomExtensionAttributeError,
InvalidAttributeTypeError, InvalidAttributeTypeError,
InvalidAttributeValueError,
MissingRequiredAttributeError, MissingRequiredAttributeError,
) )
@ -33,21 +34,25 @@ def test_missing_required_attributes() -> None:
expected_errors = { expected_errors = {
"id": [ "id": [
str(MissingRequiredAttributeError("id")), str(MissingRequiredAttributeError("id")),
str(InvalidAttributeTypeError("Attribute 'id' must not be None")), str(InvalidAttributeValueError("id", "Attribute 'id' must not be None")),
str(InvalidAttributeTypeError("Attribute 'id' must be a string")), str(InvalidAttributeTypeError("id", str)),
], ],
"source": [ "source": [
str(MissingRequiredAttributeError("source")), str(MissingRequiredAttributeError("source")),
str(InvalidAttributeTypeError("Attribute 'source' must be a string")), str(InvalidAttributeTypeError("source", str)),
], ],
"type": [ "type": [
str(MissingRequiredAttributeError("type")), str(MissingRequiredAttributeError("type")),
str(InvalidAttributeTypeError("Attribute 'type' must be a string")), str(InvalidAttributeTypeError("type", str)),
], ],
"specversion": [ "specversion": [
str(MissingRequiredAttributeError("specversion")), str(MissingRequiredAttributeError("specversion")),
str(InvalidAttributeTypeError("Attribute 'specversion' must be a string")), str(InvalidAttributeTypeError("specversion", str)),
str(InvalidAttributeTypeError("Attribute 'specversion' must be '1.0'")), str(
InvalidAttributeValueError(
"specversion", "Attribute 'specversion' must be '1.0'"
)
),
], ],
} }
@ -62,23 +67,15 @@ def test_missing_required_attributes() -> None:
[ [
( (
"2023-10-25T17:09:19.736166Z", "2023-10-25T17:09:19.736166Z",
{ {"time": [str(InvalidAttributeTypeError("time", datetime))]},
"time": [
str(
InvalidAttributeTypeError(
"Attribute 'time' must be a datetime object"
)
)
]
},
), ),
( (
datetime(2023, 10, 25, 17, 9, 19, 736166), datetime(2023, 10, 25, 17, 9, 19, 736166),
{ {
"time": [ "time": [
str( str(
InvalidAttributeTypeError( InvalidAttributeValueError(
"Attribute 'time' must be timezone aware" "time", "Attribute 'time' must be timezone aware"
) )
) )
] ]
@ -86,15 +83,7 @@ def test_missing_required_attributes() -> None:
), ),
( (
1, 1,
{ {"time": [str(InvalidAttributeTypeError("time", datetime))]},
"time": [
str(
InvalidAttributeTypeError(
"Attribute 'time' must be a datetime object"
)
)
]
},
), ),
], ],
) )
@ -120,23 +109,15 @@ def test_time_validation(time: Any, expected_error: dict) -> None:
[ [
( (
1234, 1234,
{ {"subject": [str(InvalidAttributeTypeError("subject", str))]},
"subject": [
str(
InvalidAttributeTypeError(
"Attribute 'subject' must be a string"
)
)
]
},
), ),
( (
"", "",
{ {
"subject": [ "subject": [
str( str(
InvalidAttributeTypeError( InvalidAttributeValueError(
"Attribute 'subject' must not be empty" "subject", "Attribute 'subject' must not be empty"
) )
) )
] ]
@ -169,11 +150,7 @@ def test_subject_validation(subject: Any, expected_error: dict) -> None:
1234, 1234,
{ {
"datacontenttype": [ "datacontenttype": [
str( str(InvalidAttributeTypeError("datacontenttype", str))
InvalidAttributeTypeError(
"Attribute 'datacontenttype' must be a string"
)
)
] ]
}, },
), ),
@ -182,8 +159,9 @@ def test_subject_validation(subject: Any, expected_error: dict) -> None:
{ {
"datacontenttype": [ "datacontenttype": [
str( str(
InvalidAttributeTypeError( InvalidAttributeValueError(
"Attribute 'datacontenttype' must not be empty" "datacontenttype",
"Attribute 'datacontenttype' must not be empty",
) )
) )
] ]
@ -214,23 +192,15 @@ def test_datacontenttype_validation(datacontenttype: Any, expected_error: dict)
[ [
( (
1234, 1234,
{ {"dataschema": [str(InvalidAttributeTypeError("dataschema", str))]},
"dataschema": [
str(
InvalidAttributeTypeError(
"Attribute 'dataschema' must be a string"
)
)
]
},
), ),
( (
"", "",
{ {
"dataschema": [ "dataschema": [
str( str(
InvalidAttributeTypeError( InvalidAttributeValueError(
"Attribute 'dataschema' must not be empty" "dataschema", "Attribute 'dataschema' must not be empty"
) )
) )
] ]
@ -265,12 +235,14 @@ def test_dataschema_validation(dataschema: Any, expected_error: dict) -> None:
"": [ "": [
str( str(
CustomExtensionAttributeError( CustomExtensionAttributeError(
"Extension attribute '' should be between 1 and 20 characters long" "",
"Extension attribute '' should be between 1 and 20 characters long",
) )
), ),
str( str(
CustomExtensionAttributeError( CustomExtensionAttributeError(
"Extension attribute '' should only contain lowercase letters and numbers" "",
"Extension attribute '' should only contain lowercase letters and numbers",
) )
), ),
] ]
@ -282,7 +254,8 @@ def test_dataschema_validation(dataschema: Any, expected_error: dict) -> None:
"thisisaverylongextension": [ "thisisaverylongextension": [
str( str(
CustomExtensionAttributeError( CustomExtensionAttributeError(
"Extension attribute 'thisisaverylongextension' should be between 1 and 20 characters long" "thisisaverylongextension",
"Extension attribute 'thisisaverylongextension' should be between 1 and 20 characters long",
) )
) )
] ]
@ -294,7 +267,8 @@ def test_dataschema_validation(dataschema: Any, expected_error: dict) -> None:
"data": [ "data": [
str( str(
CustomExtensionAttributeError( CustomExtensionAttributeError(
"Extension attribute 'data' is reserved and must not be used" "data",
"Extension attribute 'data' is reserved and must not be used",
) )
) )
] ]