Skip to content

Commit

Permalink
Pydantic v2 native implementation (#219)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
febus982 and pre-commit-ci[bot] authored Sep 20, 2023
1 parent e5f76ed commit 5a1063e
Show file tree
Hide file tree
Showing 12 changed files with 790 additions and 240 deletions.
25 changes: 23 additions & 2 deletions cloudevents/pydantic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,28 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from cloudevents.pydantic.conversion import from_dict, from_http, from_json
from cloudevents.pydantic.event import CloudEvent

from cloudevents.exceptions import PydanticFeatureNotInstalled

try:
from pydantic import VERSION as PYDANTIC_VERSION

pydantic_major_version = PYDANTIC_VERSION.split(".")[0]
if pydantic_major_version == "1":
from cloudevents.pydantic.v1 import CloudEvent, from_dict, from_http, from_json

else:
from cloudevents.pydantic.v2 import ( # type: ignore
CloudEvent,
from_dict,
from_http,
from_json,
)

except ImportError: # pragma: no cover # hard to test
raise PydanticFeatureNotInstalled(
"CloudEvents pydantic feature is not installed. "
"Install it using pip install cloudevents[pydantic]"
)

__all__ = ["CloudEvent", "from_json", "from_dict", "from_http"]
142 changes: 142 additions & 0 deletions cloudevents/pydantic/fields_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Copyright 2018-Present The CloudEvents Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from cloudevents.sdk.event import attribute

FIELD_DESCRIPTIONS = {
"data": {
"title": "Event Data",
"description": (
"CloudEvents MAY include domain-specific information about the occurrence."
" When present, this information will be encapsulated within data.It is"
" encoded into a media format which is specified by the datacontenttype"
" attribute (e.g. application/json), and adheres to the dataschema format"
" when those respective attributes are present."
),
},
"source": {
"title": "Event Source",
"description": (
"Identifies the context in which an event happened. Often this will include"
" information such as the type of the event source, the organization"
" publishing the event or the process that produced the event. The exact"
" syntax and semantics behind the data encoded in the URI is defined by the"
" event producer.\n"
"\n"
"Producers MUST ensure that source + id is unique for"
" each distinct event.\n"
"\n"
"An application MAY assign a unique source to each"
" distinct producer, which makes it easy to produce unique IDs since no"
" other producer will have the same source. The application MAY use UUIDs,"
" URNs, DNS authorities or an application-specific scheme to create unique"
" source identifiers.\n"
"\n"
"A source MAY include more than one producer. In"
" that case the producers MUST collaborate to ensure that source + id is"
" unique for each distinct event."
),
"example": "https://github.com/cloudevents",
},
"id": {
"title": "Event ID",
"description": (
"Identifies the event. Producers MUST ensure that source + id is unique for"
" each distinct event. If a duplicate event is re-sent (e.g. due to a"
" network error) it MAY have the same id. Consumers MAY assume that Events"
" with identical source and id are duplicates. MUST be unique within the"
" scope of the producer"
),
"example": "A234-1234-1234",
},
"type": {
"title": "Event Type",
"description": (
"This attribute contains a value describing the type of event related to"
" the originating occurrence. Often this attribute is used for routing,"
" observability, policy enforcement, etc. The format of this is producer"
" defined and might include information such as the version of the type"
),
"example": "com.github.pull_request.opened",
},
"specversion": {
"title": "Specification Version",
"description": (
"The version of the CloudEvents specification which the event uses. This"
" enables the interpretation of the context.\n"
"\n"
"Currently, this attribute will only have the 'major'"
" and 'minor' version numbers included in it. This allows for 'patch'"
" changes to the specification to be made without changing this property's"
" value in the serialization."
),
"example": attribute.DEFAULT_SPECVERSION,
},
"time": {
"title": "Occurrence Time",
"description": (
" Timestamp of when the occurrence happened. If the time of the occurrence"
" cannot be determined then this attribute MAY be set to some other time"
" (such as the current time) by the CloudEvents producer, however all"
" producers for the same source MUST be consistent in this respect. In"
" other words, either they all use the actual time of the occurrence or"
" they all use the same algorithm to determine the value used."
),
"example": "2018-04-05T17:31:00Z",
},
"subject": {
"title": "Event Subject",
"description": (
"This describes the subject of the event in the context of the event"
" producer (identified by source). In publish-subscribe scenarios, a"
" subscriber will typically subscribe to events emitted by a source, but"
" the source identifier alone might not be sufficient as a qualifier for"
" any specific event if the source context has internal"
" sub-structure.\n"
"\n"
"Identifying the subject of the event in context"
" metadata (opposed to only in the data payload) is particularly helpful in"
" generic subscription filtering scenarios where middleware is unable to"
" interpret the data content. In the above example, the subscriber might"
" only be interested in blobs with names ending with '.jpg' or '.jpeg' and"
" the subject attribute allows for constructing a simple and efficient"
" string-suffix filter for that subset of events."
),
"example": "123",
},
"datacontenttype": {
"title": "Event Data Content Type",
"description": (
"Content type of data value. This attribute enables data to carry any type"
" of content, whereby format and encoding might differ from that of the"
" chosen event format."
),
"example": "text/xml",
},
"dataschema": {
"title": "Event Data Schema",
"description": (
"Identifies the schema that data adheres to. "
"Incompatible changes to the schema SHOULD be reflected by a different URI"
),
},
}

"""
The dictionary above contains title, description, example and other
NON-FUNCTIONAL data for pydantic fields. It could be potentially.
used across all the SDK.
Functional field configurations (e.g. defaults) are still defined
in the pydantic model classes.
"""
18 changes: 18 additions & 0 deletions cloudevents/pydantic/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2018-Present The CloudEvents Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from cloudevents.pydantic.v1.conversion import from_dict, from_http, from_json
from cloudevents.pydantic.v1.event import CloudEvent

__all__ = ["CloudEvent", "from_json", "from_dict", "from_http"]
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from cloudevents.conversion import from_dict as _abstract_from_dict
from cloudevents.conversion import from_http as _abstract_from_http
from cloudevents.conversion import from_json as _abstract_from_json
from cloudevents.pydantic.event import CloudEvent
from cloudevents.pydantic.v1.event import CloudEvent
from cloudevents.sdk import types


Expand Down
130 changes: 29 additions & 101 deletions cloudevents/pydantic/event.py → cloudevents/pydantic/v1/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import typing

from cloudevents.exceptions import PydanticFeatureNotInstalled
from cloudevents.pydantic.fields_docs import FIELD_DESCRIPTIONS

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

data: typing.Optional[typing.Any] = Field(
title="Event Data",
description=(
"CloudEvents MAY include domain-specific information about the occurrence."
" When present, this information will be encapsulated within data.It is"
" encoded into a media format which is specified by the datacontenttype"
" attribute (e.g. application/json), and adheres to the dataschema format"
" when those respective attributes are present."
),
title=FIELD_DESCRIPTIONS["data"].get("title"),
description=FIELD_DESCRIPTIONS["data"].get("description"),
example=FIELD_DESCRIPTIONS["data"].get("example"),
)
source: str = Field(
title="Event Source",
description=(
"Identifies the context in which an event happened. Often this will include"
" information such as the type of the event source, the organization"
" publishing the event or the process that produced the event. The exact"
" syntax and semantics behind the data encoded in the URI is defined by the"
" event producer.\n"
"\n"
"Producers MUST ensure that source + id is unique for"
" each distinct event.\n"
"\n"
"An application MAY assign a unique source to each"
" distinct producer, which makes it easy to produce unique IDs since no"
" other producer will have the same source. The application MAY use UUIDs,"
" URNs, DNS authorities or an application-specific scheme to create unique"
" source identifiers.\n"
"\n"
"A source MAY include more than one producer. In"
" that case the producers MUST collaborate to ensure that source + id is"
" unique for each distinct event."
),
example="https://github.com/cloudevents",
title=FIELD_DESCRIPTIONS["source"].get("title"),
description=FIELD_DESCRIPTIONS["source"].get("description"),
example=FIELD_DESCRIPTIONS["source"].get("example"),
)

id: str = Field(
title=FIELD_DESCRIPTIONS["id"].get("title"),
description=FIELD_DESCRIPTIONS["id"].get("description"),
example=FIELD_DESCRIPTIONS["id"].get("example"),
default_factory=attribute.default_id_selection_algorithm,
title="Event ID",
description=(
"Identifies the event. Producers MUST ensure that source + id is unique for"
" each distinct event. If a duplicate event is re-sent (e.g. due to a"
" network error) it MAY have the same id. Consumers MAY assume that Events"
" with identical source and id are duplicates. MUST be unique within the"
" scope of the producer"
),
example="A234-1234-1234",
)
type: str = Field(
title="Event Type",
description=(
"This attribute contains a value describing the type of event related to"
" the originating occurrence. Often this attribute is used for routing,"
" observability, policy enforcement, etc. The format of this is producer"
" defined and might include information such as the version of the type"
),
example="com.github.pull_request.opened",
title=FIELD_DESCRIPTIONS["type"].get("title"),
description=FIELD_DESCRIPTIONS["type"].get("description"),
example=FIELD_DESCRIPTIONS["type"].get("example"),
)
specversion: attribute.SpecVersion = Field(
title=FIELD_DESCRIPTIONS["specversion"].get("title"),
description=FIELD_DESCRIPTIONS["specversion"].get("description"),
example=FIELD_DESCRIPTIONS["specversion"].get("example"),
default=attribute.DEFAULT_SPECVERSION,
title="Specification Version",
description=(
"The version of the CloudEvents specification which the event uses. This"
" enables the interpretation of the context.\n"
"\n"
"Currently, this attribute will only have the 'major'"
" and 'minor' version numbers included in it. This allows for 'patch'"
" changes to the specification to be made without changing this property's"
" value in the serialization."
),
example=attribute.DEFAULT_SPECVERSION,
)
time: typing.Optional[datetime.datetime] = Field(
title=FIELD_DESCRIPTIONS["time"].get("title"),
description=FIELD_DESCRIPTIONS["time"].get("description"),
example=FIELD_DESCRIPTIONS["time"].get("example"),
default_factory=attribute.default_time_selection_algorithm,
title="Occurrence Time",
description=(
" Timestamp of when the occurrence happened. If the time of the occurrence"
" cannot be determined then this attribute MAY be set to some other time"
" (such as the current time) by the CloudEvents producer, however all"
" producers for the same source MUST be consistent in this respect. In"
" other words, either they all use the actual time of the occurrence or"
" they all use the same algorithm to determine the value used."
),
example="2018-04-05T17:31:00Z",
)

subject: typing.Optional[str] = Field(
title="Event Subject",
description=(
"This describes the subject of the event in the context of the event"
" producer (identified by source). In publish-subscribe scenarios, a"
" subscriber will typically subscribe to events emitted by a source, but"
" the source identifier alone might not be sufficient as a qualifier for"
" any specific event if the source context has internal"
" sub-structure.\n"
"\n"
"Identifying the subject of the event in context"
" metadata (opposed to only in the data payload) is particularly helpful in"
" generic subscription filtering scenarios where middleware is unable to"
" interpret the data content. In the above example, the subscriber might"
" only be interested in blobs with names ending with '.jpg' or '.jpeg' and"
" the subject attribute allows for constructing a simple and efficient"
" string-suffix filter for that subset of events."
),
example="123",
title=FIELD_DESCRIPTIONS["subject"].get("title"),
description=FIELD_DESCRIPTIONS["subject"].get("description"),
example=FIELD_DESCRIPTIONS["subject"].get("example"),
)
datacontenttype: typing.Optional[str] = Field(
title="Event Data Content Type",
description=(
"Content type of data value. This attribute enables data to carry any type"
" of content, whereby format and encoding might differ from that of the"
" chosen event format."
),
example="text/xml",
title=FIELD_DESCRIPTIONS["datacontenttype"].get("title"),
description=FIELD_DESCRIPTIONS["datacontenttype"].get("description"),
example=FIELD_DESCRIPTIONS["datacontenttype"].get("example"),
)
dataschema: typing.Optional[str] = Field(
title="Event Data Schema",
description=(
"Identifies the schema that data adheres to. "
"Incompatible changes to the schema SHOULD be reflected by a different URI"
),
title=FIELD_DESCRIPTIONS["dataschema"].get("title"),
description=FIELD_DESCRIPTIONS["dataschema"].get("description"),
example=FIELD_DESCRIPTIONS["dataschema"].get("example"),
)

def __init__( # type: ignore[no-untyped-def]
Expand Down
18 changes: 18 additions & 0 deletions cloudevents/pydantic/v2/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2018-Present The CloudEvents Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from cloudevents.pydantic.v2.conversion import from_dict, from_http, from_json
from cloudevents.pydantic.v2.event import CloudEvent

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

0 comments on commit 5a1063e

Please sign in to comment.