Skip to content

Commit 4d7434f

Browse files
committed
feat: base CloudEvent class as per v1 specs, including attribute validation
1 parent 9101ab4 commit 4d7434f

File tree

5 files changed

+229
-0
lines changed

5 files changed

+229
-0
lines changed

src/cloudevents/core/v1/__init__.py

Whitespace-only changes.

src/cloudevents/core/v1/event.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from typing import Optional
2+
from datetime import datetime
3+
4+
REQUIRED_ATTRIBUTES = {"id", "source", "type", "specversion"}
5+
OPTIONAL_ATTRIBUTES = {"datacontenttype", "dataschema", "subject", "time"}
6+
7+
8+
class CloudEvent:
9+
def __init__(self, attributes: dict, data: Optional[dict] = None):
10+
self.__validate_attribute(attributes)
11+
self._attributes = attributes
12+
self._data = data
13+
14+
def __validate_attribute(self, attributes: dict):
15+
missing_attributes = [
16+
attr for attr in REQUIRED_ATTRIBUTES if attr not in attributes
17+
]
18+
if missing_attributes:
19+
raise ValueError(
20+
f"Missing required attribute(s): {', '.join(missing_attributes)}"
21+
)
22+
23+
if attributes["id"] is None:
24+
raise ValueError("Attribute 'id' must not be None")
25+
if not isinstance(attributes["id"], str):
26+
raise TypeError("Attribute 'id' must be a string")
27+
28+
if not isinstance(attributes["source"], str):
29+
raise TypeError("Attribute 'source' must be a string")
30+
31+
if not isinstance(attributes["type"], str):
32+
raise TypeError("Attribute 'type' must be a string")
33+
34+
if not isinstance(attributes["specversion"], str):
35+
raise TypeError("Attribute 'specversion' must be a string")
36+
if attributes["specversion"] != "1.0":
37+
raise ValueError("Attribute 'specversion' must be '1.0'")
38+
39+
if "time" in attributes:
40+
if not isinstance(attributes["time"], datetime):
41+
raise TypeError("Attribute 'time' must be a datetime object")
42+
43+
if not attributes["time"].tzinfo:
44+
raise ValueError("Attribute 'time' must be timezone aware")
45+
46+
if "subject" in attributes:
47+
if not isinstance(attributes["subject"], str):
48+
raise TypeError("Attribute 'subject' must be a string")
49+
50+
if not attributes["subject"]:
51+
raise ValueError("Attribute 'subject' must not be empty")
52+
53+
if "datacontenttype" in attributes:
54+
if not isinstance(attributes["datacontenttype"], str):
55+
raise TypeError("Attribute 'datacontenttype' must be a string")
56+
57+
if not attributes["datacontenttype"]:
58+
raise ValueError("Attribute 'datacontenttype' must not be empty")
59+
60+
if "dataschema" in attributes:
61+
if not isinstance(attributes["dataschema"], str):
62+
raise TypeError("Attribute 'dataschema' must be a string")
63+
64+
if not attributes["dataschema"]:
65+
raise ValueError("Attribute 'dataschema' must not be empty")
66+
67+
def get_attribute(self, attribute: str):
68+
return self._attributes[attribute]
69+
70+
def get_data(self):
71+
return self._data

tests/test_core/__init__.py

Whitespace-only changes.

tests/test_core/test_v1/__init__.py

Whitespace-only changes.

tests/test_core/test_v1/test_event.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
from cloudevents.core.v1.event import CloudEvent
2+
3+
import pytest
4+
from datetime import datetime
5+
6+
7+
@pytest.mark.parametrize(
8+
"attributes, missing_attribute",
9+
[
10+
({"source": "/", "type": "test", "specversion": "1.0"}, "id"),
11+
({"id": "1", "type": "test", "specversion": "1.0"}, "source"),
12+
({"id": "1", "source": "/", "specversion": "1.0"}, "type"),
13+
({"id": "1", "source": "/", "type": "test"}, "specversion"),
14+
],
15+
)
16+
def test_missing_required_attribute(attributes, missing_attribute):
17+
with pytest.raises(ValueError) as e:
18+
CloudEvent(attributes)
19+
20+
assert str(e.value) == f"Missing required attribute(s): {missing_attribute}"
21+
22+
23+
@pytest.mark.parametrize(
24+
"id,error",
25+
[
26+
(None, "Attribute 'id' must not be None"),
27+
(12, "Attribute 'id' must be a string"),
28+
],
29+
)
30+
def test_id_validation(id, error):
31+
with pytest.raises((ValueError, TypeError)) as e:
32+
CloudEvent({"id": id, "source": "/", "type": "test", "specversion": "1.0"})
33+
34+
assert str(e.value) == error
35+
36+
37+
@pytest.mark.parametrize("source,error", [(123, "Attribute 'source' must be a string")])
38+
def test_source_validation(source, error):
39+
with pytest.raises((ValueError, TypeError)) as e:
40+
CloudEvent({"id": "1", "source": source, "type": "test", "specversion": "1.0"})
41+
42+
assert str(e.value) == error
43+
44+
45+
@pytest.mark.parametrize(
46+
"specversion,error",
47+
[
48+
(1.0, "Attribute 'specversion' must be a string"),
49+
("1.4", "Attribute 'specversion' must be '1.0'"),
50+
],
51+
)
52+
def test_specversion_validation(specversion, error):
53+
with pytest.raises((ValueError, TypeError)) as e:
54+
CloudEvent(
55+
{"id": "1", "source": "/", "type": "test", "specversion": specversion}
56+
)
57+
58+
assert str(e.value) == error
59+
60+
61+
@pytest.mark.parametrize(
62+
"time,error",
63+
[
64+
("2023-10-25T17:09:19.736166Z", "Attribute 'time' must be a datetime object"),
65+
(
66+
datetime(2023, 10, 25, 17, 9, 19, 736166),
67+
"Attribute 'time' must be timezone aware",
68+
),
69+
],
70+
)
71+
def test_time_validation(time, error):
72+
with pytest.raises((ValueError, TypeError)) as e:
73+
CloudEvent(
74+
{
75+
"id": "1",
76+
"source": "/",
77+
"type": "test",
78+
"specversion": "1.0",
79+
"time": time,
80+
}
81+
)
82+
83+
assert str(e.value) == error
84+
85+
86+
@pytest.mark.parametrize(
87+
"subject,error",
88+
[
89+
(1234, "Attribute 'subject' must be a string"),
90+
(
91+
"",
92+
"Attribute 'subject' must not be empty",
93+
),
94+
],
95+
)
96+
def test_subject_validation(subject, error):
97+
with pytest.raises((ValueError, TypeError)) as e:
98+
CloudEvent(
99+
{
100+
"id": "1",
101+
"source": "/",
102+
"type": "test",
103+
"specversion": "1.0",
104+
"subject": subject,
105+
}
106+
)
107+
108+
assert str(e.value) == error
109+
110+
111+
@pytest.mark.parametrize(
112+
"datacontenttype,error",
113+
[
114+
(1234, "Attribute 'datacontenttype' must be a string"),
115+
(
116+
"",
117+
"Attribute 'datacontenttype' must not be empty",
118+
),
119+
],
120+
)
121+
def test_datacontenttype_validation(datacontenttype, error):
122+
with pytest.raises((ValueError, TypeError)) as e:
123+
CloudEvent(
124+
{
125+
"id": "1",
126+
"source": "/",
127+
"type": "test",
128+
"specversion": "1.0",
129+
"datacontenttype": datacontenttype,
130+
}
131+
)
132+
133+
assert str(e.value) == error
134+
135+
136+
@pytest.mark.parametrize(
137+
"dataschema,error",
138+
[
139+
(1234, "Attribute 'dataschema' must be a string"),
140+
(
141+
"",
142+
"Attribute 'dataschema' must not be empty",
143+
),
144+
],
145+
)
146+
def test_dataschema_validation(dataschema, error):
147+
with pytest.raises((ValueError, TypeError)) as e:
148+
CloudEvent(
149+
{
150+
"id": "1",
151+
"source": "/",
152+
"type": "test",
153+
"specversion": "1.0",
154+
"dataschema": dataschema,
155+
}
156+
)
157+
158+
assert str(e.value) == error

0 commit comments

Comments
 (0)