|
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