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:
parent
1d43d688be
commit
21493e180f
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue