|
12 | 12 | # License for the specific language governing permissions and limitations |
13 | 13 | # under the License. |
14 | 14 |
|
15 | | -from typing import Any, Optional, Final |
16 | | -from datetime import datetime |
17 | 15 | import re |
| 16 | +from datetime import datetime |
| 17 | +from typing import Any, Final, Optional |
| 18 | + |
| 19 | +from cloudevents.core.v1.exceptions import CloudEventValidationError |
18 | 20 |
|
19 | | -REQUIRED_ATTRIBUTES: Final[set[str]] = {"id", "source", "type", "specversion"} |
20 | | -OPTIONAL_ATTRIBUTES: Final[set[str]] = { |
| 21 | +REQUIRED_ATTRIBUTES: Final[list[str]] = ["id", "source", "type", "specversion"] |
| 22 | +OPTIONAL_ATTRIBUTES: Final[list[str]] = [ |
21 | 23 | "datacontenttype", |
22 | 24 | "dataschema", |
23 | 25 | "subject", |
24 | 26 | "time", |
25 | | -} |
| 27 | +] |
26 | 28 |
|
27 | 29 |
|
28 | 30 | class CloudEvent: |
@@ -55,102 +57,129 @@ def _validate_attribute(attributes: dict[str, Any]) -> None: |
55 | 57 |
|
56 | 58 | See https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#required-attributes |
57 | 59 | """ |
58 | | - CloudEvent._validate_required_attributes(attributes) |
59 | | - CloudEvent._validate_attribute_types(attributes) |
60 | | - CloudEvent._validate_optional_attributes(attributes) |
61 | | - CloudEvent._validate_extension_attributes(attributes) |
| 60 | + errors = {} |
| 61 | + errors.update(CloudEvent._validate_required_attributes(attributes)) |
| 62 | + errors.update(CloudEvent._validate_attribute_types(attributes)) |
| 63 | + errors.update(CloudEvent._validate_optional_attributes(attributes)) |
| 64 | + errors.update(CloudEvent._validate_extension_attributes(attributes)) |
| 65 | + if errors: |
| 66 | + raise CloudEventValidationError(errors) |
62 | 67 |
|
63 | 68 | @staticmethod |
64 | | - def _validate_required_attributes(attributes: dict[str, Any]) -> None: |
| 69 | + def _validate_required_attributes( |
| 70 | + attributes: dict[str, Any], |
| 71 | + ) -> dict[str, list[str]]: |
65 | 72 | """ |
66 | 73 | Validates that all required attributes are present. |
67 | 74 |
|
68 | 75 | :param attributes: The attributes of the CloudEvent instance. |
69 | | - :raises ValueError: If any of the required attributes are missing. |
| 76 | + :return: A dictionary of validation error messages. |
70 | 77 | """ |
| 78 | + errors = {} |
71 | 79 | missing_attributes = [ |
72 | 80 | attr for attr in REQUIRED_ATTRIBUTES if attr not in attributes |
73 | 81 | ] |
74 | 82 | if missing_attributes: |
75 | | - raise ValueError( |
| 83 | + errors["required"] = [ |
76 | 84 | f"Missing required attribute(s): {', '.join(missing_attributes)}" |
77 | | - ) |
| 85 | + ] |
| 86 | + return errors |
78 | 87 |
|
79 | 88 | @staticmethod |
80 | | - def _validate_attribute_types(attributes: dict[str, Any]) -> None: |
| 89 | + def _validate_attribute_types(attributes: dict[str, Any]) -> dict[str, list[str]]: |
81 | 90 | """ |
82 | 91 | Validates the types of the required attributes. |
83 | 92 |
|
84 | 93 | :param attributes: The attributes of the CloudEvent instance. |
85 | | - :raises ValueError: If any of the required attributes have invalid values. |
86 | | - :raises TypeError: If any of the required attributes have invalid types. |
87 | | - """ |
88 | | - if attributes["id"] is None: |
89 | | - raise ValueError("Attribute 'id' must not be None") |
90 | | - if not isinstance(attributes["id"], str): |
91 | | - raise TypeError("Attribute 'id' must be a string") |
92 | | - if not isinstance(attributes["source"], str): |
93 | | - raise TypeError("Attribute 'source' must be a string") |
94 | | - if not isinstance(attributes["type"], str): |
95 | | - raise TypeError("Attribute 'type' must be a string") |
96 | | - if not isinstance(attributes["specversion"], str): |
97 | | - raise TypeError("Attribute 'specversion' must be a string") |
98 | | - if attributes["specversion"] != "1.0": |
99 | | - raise ValueError("Attribute 'specversion' must be '1.0'") |
| 94 | + :return: A dictionary of validation error messages. |
| 95 | + """ |
| 96 | + errors = {} |
| 97 | + type_errors = [] |
| 98 | + if attributes.get("id") is None: |
| 99 | + type_errors.append("Attribute 'id' must not be None") |
| 100 | + if not isinstance(attributes.get("id"), str): |
| 101 | + type_errors.append("Attribute 'id' must be a string") |
| 102 | + if not isinstance(attributes.get("source"), str): |
| 103 | + type_errors.append("Attribute 'source' must be a string") |
| 104 | + if not isinstance(attributes.get("type"), str): |
| 105 | + type_errors.append("Attribute 'type' must be a string") |
| 106 | + if not isinstance(attributes.get("specversion"), str): |
| 107 | + type_errors.append("Attribute 'specversion' must be a string") |
| 108 | + if attributes.get("specversion") != "1.0": |
| 109 | + type_errors.append("Attribute 'specversion' must be '1.0'") |
| 110 | + if type_errors: |
| 111 | + errors["type"] = type_errors |
| 112 | + return errors |
100 | 113 |
|
101 | 114 | @staticmethod |
102 | | - def _validate_optional_attributes(attributes: dict[str, Any]) -> None: |
| 115 | + def _validate_optional_attributes( |
| 116 | + attributes: dict[str, Any], |
| 117 | + ) -> dict[str, list[str]]: |
103 | 118 | """ |
104 | 119 | Validates the types and values of the optional attributes. |
105 | 120 |
|
106 | 121 | :param attributes: The attributes of the CloudEvent instance. |
107 | | - :raises ValueError: If any of the optional attributes have invalid values. |
108 | | - :raises TypeError: If any of the optional attributes have invalid types. |
| 122 | + :return: A dictionary of validation error messages. |
109 | 123 | """ |
| 124 | + errors = {} |
| 125 | + optional_errors = [] |
110 | 126 | if "time" in attributes: |
111 | 127 | if not isinstance(attributes["time"], datetime): |
112 | | - raise TypeError("Attribute 'time' must be a datetime object") |
113 | | - if not attributes["time"].tzinfo: |
114 | | - raise ValueError("Attribute 'time' must be timezone aware") |
| 128 | + optional_errors.append("Attribute 'time' must be a datetime object") |
| 129 | + if hasattr(attributes["time"], "tzinfo") and not attributes["time"].tzinfo: |
| 130 | + optional_errors.append("Attribute 'time' must be timezone aware") |
115 | 131 | if "subject" in attributes: |
116 | 132 | if not isinstance(attributes["subject"], str): |
117 | | - raise TypeError("Attribute 'subject' must be a string") |
| 133 | + optional_errors.append("Attribute 'subject' must be a string") |
118 | 134 | if not attributes["subject"]: |
119 | | - raise ValueError("Attribute 'subject' must not be empty") |
| 135 | + optional_errors.append("Attribute 'subject' must not be empty") |
120 | 136 | if "datacontenttype" in attributes: |
121 | 137 | if not isinstance(attributes["datacontenttype"], str): |
122 | | - raise TypeError("Attribute 'datacontenttype' must be a string") |
| 138 | + optional_errors.append("Attribute 'datacontenttype' must be a string") |
123 | 139 | if not attributes["datacontenttype"]: |
124 | | - raise ValueError("Attribute 'datacontenttype' must not be empty") |
| 140 | + optional_errors.append("Attribute 'datacontenttype' must not be empty") |
125 | 141 | if "dataschema" in attributes: |
126 | 142 | if not isinstance(attributes["dataschema"], str): |
127 | | - raise TypeError("Attribute 'dataschema' must be a string") |
| 143 | + optional_errors.append("Attribute 'dataschema' must be a string") |
128 | 144 | if not attributes["dataschema"]: |
129 | | - raise ValueError("Attribute 'dataschema' must not be empty") |
| 145 | + optional_errors.append("Attribute 'dataschema' must not be empty") |
| 146 | + if optional_errors: |
| 147 | + errors["optional"] = optional_errors |
| 148 | + return errors |
130 | 149 |
|
131 | 150 | @staticmethod |
132 | | - def _validate_extension_attributes(attributes: dict[str, Any]) -> None: |
| 151 | + def _validate_extension_attributes( |
| 152 | + attributes: dict[str, Any], |
| 153 | + ) -> dict[str, list[str]]: |
133 | 154 | """ |
134 | 155 | Validates the extension attributes. |
135 | 156 |
|
136 | 157 | :param attributes: The attributes of the CloudEvent instance. |
137 | | - :raises ValueError: If any of the extension attributes have invalid values. |
138 | | - """ |
139 | | - for extension_attributes in ( |
140 | | - set(attributes.keys()) - REQUIRED_ATTRIBUTES - OPTIONAL_ATTRIBUTES |
141 | | - ): |
142 | | - if extension_attributes == "data": |
143 | | - raise ValueError( |
| 158 | + :return: A dictionary of validation error messages. |
| 159 | + """ |
| 160 | + errors = {} |
| 161 | + extension_errors = [] |
| 162 | + extension_attributes = [ |
| 163 | + key |
| 164 | + for key in attributes.keys() |
| 165 | + if key not in REQUIRED_ATTRIBUTES and key not in OPTIONAL_ATTRIBUTES |
| 166 | + ] |
| 167 | + for extension_attribute in extension_attributes: |
| 168 | + if extension_attribute == "data": |
| 169 | + extension_errors.append( |
144 | 170 | "Extension attribute 'data' is reserved and must not be used" |
145 | 171 | ) |
146 | | - if not (1 <= len(extension_attributes) <= 20): |
147 | | - raise ValueError( |
148 | | - f"Extension attribute '{extension_attributes}' should be between 1 and 20 characters long" |
| 172 | + if not (1 <= len(extension_attribute) <= 20): |
| 173 | + extension_errors.append( |
| 174 | + f"Extension attribute '{extension_attribute}' should be between 1 and 20 characters long" |
149 | 175 | ) |
150 | | - if not re.match(r"^[a-z0-9]+$", extension_attributes): |
151 | | - raise ValueError( |
152 | | - f"Extension attribute '{extension_attributes}' should only contain lowercase letters and numbers" |
| 176 | + if not re.match(r"^[a-z0-9]+$", extension_attribute): |
| 177 | + extension_errors.append( |
| 178 | + f"Extension attribute '{extension_attribute}' should only contain lowercase letters and numbers" |
153 | 179 | ) |
| 180 | + if extension_errors: |
| 181 | + errors["extensions"] = extension_errors |
| 182 | + return errors |
154 | 183 |
|
155 | 184 | def get_id(self) -> str: |
156 | 185 | """ |
@@ -215,7 +244,7 @@ def get_time(self) -> Optional[datetime]: |
215 | 244 | :return: The time of the event. |
216 | 245 | """ |
217 | 246 | return self._attributes.get("time") |
218 | | - |
| 247 | + |
219 | 248 | def get_extension(self, extension_name: str) -> Any: |
220 | 249 | """ |
221 | 250 | Retrieve an extension attribute of the event. |
|
0 commit comments