Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pydantic v2 native implementation #219

Merged
merged 19 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably add "exports" here in the same style as we have it in pydantic.__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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same regarding the exports

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