Skip to content

Commit 5a1063e

Browse files
Pydantic v2 native implementation (#219)
* Create stub pydantic v2 implementation and parametrize tests for both implementations Signed-off-by: Federico Busetti <[email protected]> * Add default values to optional fields Signed-off-by: Federico Busetti <[email protected]> * Adapt pydantic v1 serializer/deserializer logic Signed-off-by: Federico Busetti <[email protected]> * Extract CloudEvent fields non functional data in separate module Signed-off-by: Federico Busetti <[email protected]> * Fix lint Signed-off-by: Federico Busetti <[email protected]> * Add missing Copyright Signed-off-by: Federico Busetti <[email protected]> * Add missing docstring Signed-off-by: Federico Busetti <[email protected]> * Remove test leftover Signed-off-by: Federico Busetti <[email protected]> * Remove dependency on HTTP CloudEvent implementation Signed-off-by: Federico Busetti <[email protected]> * Remove failing test for unsupported scenario Fix typo Signed-off-by: Federico Busetti <[email protected]> * Use SDK json serialization logic Signed-off-by: Federico Busetti <[email protected]> * No need to filter base64_data Signed-off-by: Federico Busetti <[email protected]> * Use SDK json deserialization logic Signed-off-by: Federico Busetti <[email protected]> * Fix imports Signed-off-by: Federico Busetti <[email protected]> * Move docs after field declarations Signed-off-by: Federico Busetti <[email protected]> * Add test for model_validate_json method Signed-off-by: Federico Busetti <[email protected]> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Use fully qualified imports Signed-off-by: Federico Busetti <[email protected]> * Ignore typing error Signed-off-by: Federico Busetti <[email protected]> --------- Signed-off-by: Federico Busetti <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent e5f76ed commit 5a1063e

File tree

12 files changed

+790
-240
lines changed

12 files changed

+790
-240
lines changed

cloudevents/pydantic/__init__.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,28 @@
1111
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
14-
from cloudevents.pydantic.conversion import from_dict, from_http, from_json
15-
from cloudevents.pydantic.event import CloudEvent
14+
15+
from cloudevents.exceptions import PydanticFeatureNotInstalled
16+
17+
try:
18+
from pydantic import VERSION as PYDANTIC_VERSION
19+
20+
pydantic_major_version = PYDANTIC_VERSION.split(".")[0]
21+
if pydantic_major_version == "1":
22+
from cloudevents.pydantic.v1 import CloudEvent, from_dict, from_http, from_json
23+
24+
else:
25+
from cloudevents.pydantic.v2 import ( # type: ignore
26+
CloudEvent,
27+
from_dict,
28+
from_http,
29+
from_json,
30+
)
31+
32+
except ImportError: # pragma: no cover # hard to test
33+
raise PydanticFeatureNotInstalled(
34+
"CloudEvents pydantic feature is not installed. "
35+
"Install it using pip install cloudevents[pydantic]"
36+
)
1637

1738
__all__ = ["CloudEvent", "from_json", "from_dict", "from_http"]

cloudevents/pydantic/fields_docs.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Copyright 2018-Present The CloudEvents Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from cloudevents.sdk.event import attribute
16+
17+
FIELD_DESCRIPTIONS = {
18+
"data": {
19+
"title": "Event Data",
20+
"description": (
21+
"CloudEvents MAY include domain-specific information about the occurrence."
22+
" When present, this information will be encapsulated within data.It is"
23+
" encoded into a media format which is specified by the datacontenttype"
24+
" attribute (e.g. application/json), and adheres to the dataschema format"
25+
" when those respective attributes are present."
26+
),
27+
},
28+
"source": {
29+
"title": "Event Source",
30+
"description": (
31+
"Identifies the context in which an event happened. Often this will include"
32+
" information such as the type of the event source, the organization"
33+
" publishing the event or the process that produced the event. The exact"
34+
" syntax and semantics behind the data encoded in the URI is defined by the"
35+
" event producer.\n"
36+
"\n"
37+
"Producers MUST ensure that source + id is unique for"
38+
" each distinct event.\n"
39+
"\n"
40+
"An application MAY assign a unique source to each"
41+
" distinct producer, which makes it easy to produce unique IDs since no"
42+
" other producer will have the same source. The application MAY use UUIDs,"
43+
" URNs, DNS authorities or an application-specific scheme to create unique"
44+
" source identifiers.\n"
45+
"\n"
46+
"A source MAY include more than one producer. In"
47+
" that case the producers MUST collaborate to ensure that source + id is"
48+
" unique for each distinct event."
49+
),
50+
"example": "https://github.com/cloudevents",
51+
},
52+
"id": {
53+
"title": "Event ID",
54+
"description": (
55+
"Identifies the event. Producers MUST ensure that source + id is unique for"
56+
" each distinct event. If a duplicate event is re-sent (e.g. due to a"
57+
" network error) it MAY have the same id. Consumers MAY assume that Events"
58+
" with identical source and id are duplicates. MUST be unique within the"
59+
" scope of the producer"
60+
),
61+
"example": "A234-1234-1234",
62+
},
63+
"type": {
64+
"title": "Event Type",
65+
"description": (
66+
"This attribute contains a value describing the type of event related to"
67+
" the originating occurrence. Often this attribute is used for routing,"
68+
" observability, policy enforcement, etc. The format of this is producer"
69+
" defined and might include information such as the version of the type"
70+
),
71+
"example": "com.github.pull_request.opened",
72+
},
73+
"specversion": {
74+
"title": "Specification Version",
75+
"description": (
76+
"The version of the CloudEvents specification which the event uses. This"
77+
" enables the interpretation of the context.\n"
78+
"\n"
79+
"Currently, this attribute will only have the 'major'"
80+
" and 'minor' version numbers included in it. This allows for 'patch'"
81+
" changes to the specification to be made without changing this property's"
82+
" value in the serialization."
83+
),
84+
"example": attribute.DEFAULT_SPECVERSION,
85+
},
86+
"time": {
87+
"title": "Occurrence Time",
88+
"description": (
89+
" Timestamp of when the occurrence happened. If the time of the occurrence"
90+
" cannot be determined then this attribute MAY be set to some other time"
91+
" (such as the current time) by the CloudEvents producer, however all"
92+
" producers for the same source MUST be consistent in this respect. In"
93+
" other words, either they all use the actual time of the occurrence or"
94+
" they all use the same algorithm to determine the value used."
95+
),
96+
"example": "2018-04-05T17:31:00Z",
97+
},
98+
"subject": {
99+
"title": "Event Subject",
100+
"description": (
101+
"This describes the subject of the event in the context of the event"
102+
" producer (identified by source). In publish-subscribe scenarios, a"
103+
" subscriber will typically subscribe to events emitted by a source, but"
104+
" the source identifier alone might not be sufficient as a qualifier for"
105+
" any specific event if the source context has internal"
106+
" sub-structure.\n"
107+
"\n"
108+
"Identifying the subject of the event in context"
109+
" metadata (opposed to only in the data payload) is particularly helpful in"
110+
" generic subscription filtering scenarios where middleware is unable to"
111+
" interpret the data content. In the above example, the subscriber might"
112+
" only be interested in blobs with names ending with '.jpg' or '.jpeg' and"
113+
" the subject attribute allows for constructing a simple and efficient"
114+
" string-suffix filter for that subset of events."
115+
),
116+
"example": "123",
117+
},
118+
"datacontenttype": {
119+
"title": "Event Data Content Type",
120+
"description": (
121+
"Content type of data value. This attribute enables data to carry any type"
122+
" of content, whereby format and encoding might differ from that of the"
123+
" chosen event format."
124+
),
125+
"example": "text/xml",
126+
},
127+
"dataschema": {
128+
"title": "Event Data Schema",
129+
"description": (
130+
"Identifies the schema that data adheres to. "
131+
"Incompatible changes to the schema SHOULD be reflected by a different URI"
132+
),
133+
},
134+
}
135+
136+
"""
137+
The dictionary above contains title, description, example and other
138+
NON-FUNCTIONAL data for pydantic fields. It could be potentially.
139+
used across all the SDK.
140+
Functional field configurations (e.g. defaults) are still defined
141+
in the pydantic model classes.
142+
"""

cloudevents/pydantic/v1/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright 2018-Present The CloudEvents Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from cloudevents.pydantic.v1.conversion import from_dict, from_http, from_json
16+
from cloudevents.pydantic.v1.event import CloudEvent
17+
18+
__all__ = ["CloudEvent", "from_json", "from_dict", "from_http"]

cloudevents/pydantic/conversion.py renamed to cloudevents/pydantic/v1/conversion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from cloudevents.conversion import from_dict as _abstract_from_dict
1717
from cloudevents.conversion import from_http as _abstract_from_http
1818
from cloudevents.conversion import from_json as _abstract_from_json
19-
from cloudevents.pydantic.event import CloudEvent
19+
from cloudevents.pydantic.v1.event import CloudEvent
2020
from cloudevents.sdk import types
2121

2222

cloudevents/pydantic/event.py renamed to cloudevents/pydantic/v1/event.py

Lines changed: 29 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import typing
1717

1818
from cloudevents.exceptions import PydanticFeatureNotInstalled
19+
from cloudevents.pydantic.fields_docs import FIELD_DESCRIPTIONS
1920

2021
try:
2122
from pydantic import VERSION as PYDANTIC_VERSION
@@ -72,7 +73,7 @@ def _ce_json_dumps( # type: ignore[no-untyped-def]
7273
def _ce_json_loads( # type: ignore[no-untyped-def]
7374
data: typing.AnyStr, *args, **kwargs # noqa
7475
) -> typing.Dict[typing.Any, typing.Any]:
75-
"""Perforns Pydantic-specific deserialization of the event.
76+
"""Performs Pydantic-specific deserialization of the event.
7677
7778
Needed by the pydantic base-model to de-serialize the event correctly from json.
7879
Without this function the data will be incorrectly de-serialized.
@@ -104,125 +105,52 @@ def create(
104105
return cls(attributes, data)
105106

106107
data: typing.Optional[typing.Any] = Field(
107-
title="Event Data",
108-
description=(
109-
"CloudEvents MAY include domain-specific information about the occurrence."
110-
" When present, this information will be encapsulated within data.It is"
111-
" encoded into a media format which is specified by the datacontenttype"
112-
" attribute (e.g. application/json), and adheres to the dataschema format"
113-
" when those respective attributes are present."
114-
),
108+
title=FIELD_DESCRIPTIONS["data"].get("title"),
109+
description=FIELD_DESCRIPTIONS["data"].get("description"),
110+
example=FIELD_DESCRIPTIONS["data"].get("example"),
115111
)
116112
source: str = Field(
117-
title="Event Source",
118-
description=(
119-
"Identifies the context in which an event happened. Often this will include"
120-
" information such as the type of the event source, the organization"
121-
" publishing the event or the process that produced the event. The exact"
122-
" syntax and semantics behind the data encoded in the URI is defined by the"
123-
" event producer.\n"
124-
"\n"
125-
"Producers MUST ensure that source + id is unique for"
126-
" each distinct event.\n"
127-
"\n"
128-
"An application MAY assign a unique source to each"
129-
" distinct producer, which makes it easy to produce unique IDs since no"
130-
" other producer will have the same source. The application MAY use UUIDs,"
131-
" URNs, DNS authorities or an application-specific scheme to create unique"
132-
" source identifiers.\n"
133-
"\n"
134-
"A source MAY include more than one producer. In"
135-
" that case the producers MUST collaborate to ensure that source + id is"
136-
" unique for each distinct event."
137-
),
138-
example="https://github.com/cloudevents",
113+
title=FIELD_DESCRIPTIONS["source"].get("title"),
114+
description=FIELD_DESCRIPTIONS["source"].get("description"),
115+
example=FIELD_DESCRIPTIONS["source"].get("example"),
139116
)
140-
141117
id: str = Field(
118+
title=FIELD_DESCRIPTIONS["id"].get("title"),
119+
description=FIELD_DESCRIPTIONS["id"].get("description"),
120+
example=FIELD_DESCRIPTIONS["id"].get("example"),
142121
default_factory=attribute.default_id_selection_algorithm,
143-
title="Event ID",
144-
description=(
145-
"Identifies the event. Producers MUST ensure that source + id is unique for"
146-
" each distinct event. If a duplicate event is re-sent (e.g. due to a"
147-
" network error) it MAY have the same id. Consumers MAY assume that Events"
148-
" with identical source and id are duplicates. MUST be unique within the"
149-
" scope of the producer"
150-
),
151-
example="A234-1234-1234",
152122
)
153123
type: str = Field(
154-
title="Event Type",
155-
description=(
156-
"This attribute contains a value describing the type of event related to"
157-
" the originating occurrence. Often this attribute is used for routing,"
158-
" observability, policy enforcement, etc. The format of this is producer"
159-
" defined and might include information such as the version of the type"
160-
),
161-
example="com.github.pull_request.opened",
124+
title=FIELD_DESCRIPTIONS["type"].get("title"),
125+
description=FIELD_DESCRIPTIONS["type"].get("description"),
126+
example=FIELD_DESCRIPTIONS["type"].get("example"),
162127
)
163128
specversion: attribute.SpecVersion = Field(
129+
title=FIELD_DESCRIPTIONS["specversion"].get("title"),
130+
description=FIELD_DESCRIPTIONS["specversion"].get("description"),
131+
example=FIELD_DESCRIPTIONS["specversion"].get("example"),
164132
default=attribute.DEFAULT_SPECVERSION,
165-
title="Specification Version",
166-
description=(
167-
"The version of the CloudEvents specification which the event uses. This"
168-
" enables the interpretation of the context.\n"
169-
"\n"
170-
"Currently, this attribute will only have the 'major'"
171-
" and 'minor' version numbers included in it. This allows for 'patch'"
172-
" changes to the specification to be made without changing this property's"
173-
" value in the serialization."
174-
),
175-
example=attribute.DEFAULT_SPECVERSION,
176133
)
177134
time: typing.Optional[datetime.datetime] = Field(
135+
title=FIELD_DESCRIPTIONS["time"].get("title"),
136+
description=FIELD_DESCRIPTIONS["time"].get("description"),
137+
example=FIELD_DESCRIPTIONS["time"].get("example"),
178138
default_factory=attribute.default_time_selection_algorithm,
179-
title="Occurrence Time",
180-
description=(
181-
" Timestamp of when the occurrence happened. If the time of the occurrence"
182-
" cannot be determined then this attribute MAY be set to some other time"
183-
" (such as the current time) by the CloudEvents producer, however all"
184-
" producers for the same source MUST be consistent in this respect. In"
185-
" other words, either they all use the actual time of the occurrence or"
186-
" they all use the same algorithm to determine the value used."
187-
),
188-
example="2018-04-05T17:31:00Z",
189139
)
190-
191140
subject: typing.Optional[str] = Field(
192-
title="Event Subject",
193-
description=(
194-
"This describes the subject of the event in the context of the event"
195-
" producer (identified by source). In publish-subscribe scenarios, a"
196-
" subscriber will typically subscribe to events emitted by a source, but"
197-
" the source identifier alone might not be sufficient as a qualifier for"
198-
" any specific event if the source context has internal"
199-
" sub-structure.\n"
200-
"\n"
201-
"Identifying the subject of the event in context"
202-
" metadata (opposed to only in the data payload) is particularly helpful in"
203-
" generic subscription filtering scenarios where middleware is unable to"
204-
" interpret the data content. In the above example, the subscriber might"
205-
" only be interested in blobs with names ending with '.jpg' or '.jpeg' and"
206-
" the subject attribute allows for constructing a simple and efficient"
207-
" string-suffix filter for that subset of events."
208-
),
209-
example="123",
141+
title=FIELD_DESCRIPTIONS["subject"].get("title"),
142+
description=FIELD_DESCRIPTIONS["subject"].get("description"),
143+
example=FIELD_DESCRIPTIONS["subject"].get("example"),
210144
)
211145
datacontenttype: typing.Optional[str] = Field(
212-
title="Event Data Content Type",
213-
description=(
214-
"Content type of data value. This attribute enables data to carry any type"
215-
" of content, whereby format and encoding might differ from that of the"
216-
" chosen event format."
217-
),
218-
example="text/xml",
146+
title=FIELD_DESCRIPTIONS["datacontenttype"].get("title"),
147+
description=FIELD_DESCRIPTIONS["datacontenttype"].get("description"),
148+
example=FIELD_DESCRIPTIONS["datacontenttype"].get("example"),
219149
)
220150
dataschema: typing.Optional[str] = Field(
221-
title="Event Data Schema",
222-
description=(
223-
"Identifies the schema that data adheres to. "
224-
"Incompatible changes to the schema SHOULD be reflected by a different URI"
225-
),
151+
title=FIELD_DESCRIPTIONS["dataschema"].get("title"),
152+
description=FIELD_DESCRIPTIONS["dataschema"].get("description"),
153+
example=FIELD_DESCRIPTIONS["dataschema"].get("example"),
226154
)
227155

228156
def __init__( # type: ignore[no-untyped-def]

cloudevents/pydantic/v2/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright 2018-Present The CloudEvents Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
# not use this file except in compliance with the License. You may obtain
5+
# a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12+
# License for the specific language governing permissions and limitations
13+
# under the License.
14+
15+
from cloudevents.pydantic.v2.conversion import from_dict, from_http, from_json
16+
from cloudevents.pydantic.v2.event import CloudEvent
17+
18+
__all__ = ["CloudEvent", "from_json", "from_dict", "from_http"]

0 commit comments

Comments
 (0)