Skip to content

Commit

Permalink
Reusable annotations for composition
Browse files Browse the repository at this point in the history
Signed-off-by: Federico Busetti <729029+febus982@users.noreply.github.com>
  • Loading branch information
febus982 committed Sep 29, 2024
1 parent bfbb4a2 commit b32c291
Show file tree
Hide file tree
Showing 12 changed files with 89 additions and 54 deletions.
2 changes: 1 addition & 1 deletion benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@

from cloudevents_pydantic.bindings.http import HTTPHandler
from cloudevents_pydantic.events import CloudEvent
from cloudevents_pydantic.events.field_types import Binary
from cloudevents_pydantic.events.fields.types import Binary

valid_json = '{"data_base64":"dGVzdA==","source":"https://example.com/event-producer","id":"b96267e2-87be-4f7a-b87c-82f64360d954","type":"com.example.string","specversion":"1.0","time":"2022-07-16T12:03:20.519216+04:00","subject":null,"datacontenttype":null,"dataschema":null}'
test_iterations = 1000000
Expand Down
44 changes: 24 additions & 20 deletions cloudevents_pydantic/events/_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,31 @@
# ==============================================================================
import base64
import datetime
from typing import Any, Dict, Optional, Union
from typing import Annotated, Any, Dict, Optional, Union

from pydantic import (
BaseModel,
ConfigDict,
Field,
model_serializer,
model_validator,
)
from pydantic.fields import FieldInfo
from pydantic_core.core_schema import ValidationInfo
from ulid import ULID

from .annotations import (
DataAnnotation,
DataContentTypeAnnotation,
DataSchemaAnnotation,
IdAnnotation,
SourceAnnotation,
SubjectAnnotation,
TimeAnnotation,
TypeAnnotation,
from .fields.metadata import (
FieldData,
FieldDataContentType,
FieldDataSchema,
FieldSource,
FieldSpecVersion,
FieldSubject,
FieldTime,
FieldTitle,
FieldType,
)
from .field_types import Binary, SpecVersion
from .fields.types import URI, Binary, DateTime, SpecVersion, String, URIReference

DEFAULT_SPECVERSION = SpecVersion.v1_0

Expand Down Expand Up @@ -85,19 +87,21 @@ def event_factory(
**kwargs,
)

data: DataAnnotation
data: Annotated[Any, Field(default=None), FieldData]

# Mandatory fields
source: SourceAnnotation
id: IdAnnotation
type: TypeAnnotation
specversion: SpecVersion
source: Annotated[URIReference, FieldSource]
id: Annotated[String, FieldTitle]
type: Annotated[String, FieldType]
specversion: Annotated[SpecVersion, FieldSpecVersion]

# Optional fields
time: TimeAnnotation
subject: SubjectAnnotation
datacontenttype: DataContentTypeAnnotation
dataschema: DataSchemaAnnotation
time: Annotated[Optional[DateTime], Field(default=None), FieldTime]
subject: Annotated[Optional[String], Field(default=None), FieldSubject]
datacontenttype: Annotated[
Optional[String], Field(default=None), FieldDataContentType
]
dataschema: Annotated[Optional[URI], Field(default=None), FieldDataSchema]

model_config = ConfigDict(
extra="forbid",
Expand Down
10 changes: 8 additions & 2 deletions cloudevents_pydantic/events/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from pydantic import Field

from ._attributes_field_metadata import (
from cloudevents_pydantic.events.fields.metadata import (
FieldData,
FieldDataContentType,
FieldDataSchema,
Expand All @@ -35,7 +35,13 @@
FieldTitle,
FieldType,
)
from .field_types import URI, DateTime, SpecVersion, String, URIReference
from cloudevents_pydantic.events.fields.types import (
URI,
DateTime,
SpecVersion,
String,
URIReference,
)

DataAnnotation = Annotated[Any, Field(default=None), FieldData]
SourceAnnotation = Annotated[URIReference, FieldSource]
Expand Down
23 changes: 23 additions & 0 deletions cloudevents_pydantic/events/fields/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# ==============================================================================
# Copyright (c) 2024 Federico Busetti =
# <729029+febus982@users.noreply.github.com> =
# =
# Permission is hereby granted, free of charge, to any person obtaining a =
# copy of this software and associated documentation files (the "Software"), =
# to deal in the Software without restriction, including without limitation =
# the rights to use, copy, modify, merge, publish, distribute, sublicense, =
# and/or sell copies of the Software, and to permit persons to whom the =
# Software is furnished to do so, subject to the following conditions: =
# =
# The above copyright notice and this permission notice shall be included in =
# all copies or substantial portions of the Software. =
# =
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR =
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, =
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL =
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER =
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING =
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER =
# DEALINGS IN THE SOFTWARE. =
# ==============================================================================

56 changes: 29 additions & 27 deletions docs/event_class.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,52 +93,54 @@ You can use either `str` values or python objects (see the `time` field)
///
///

## Create your own event subclasses in the right way
## Best practices when creating your event classes

When you create event types in your app you will want to make sure to follow these best practices:

* Use `TypedDict` for structured data instead of nested pydantic models (as specified in
[Pydantic performance](https://docs.pydantic.dev/latest/concepts/performance/#use-typeddict-over-nested-models)
documentatin)
* Use the fields types defined in the `cloudevents_pydantic.events.field_types`. These types will
* Use the fields types defined in the `cloudevents_pydantic.events.field.types`. These types will
be kept up to date and make sure their validation, serialization and deserialization rules
will be compliant with the [CloudEvents spec](https://github.com/cloudevents/spec/tree/main).
* Write your own pydantic `Field` for data
* Use the fields available in the `cloudevents_pydantic.events.field.metadata` when overriding
the cloudevent fields to inherit CloudEvents field descriptive metadata (i.e. title, description)
will be populated in the schema.

Example:

/// tab | class Syntax
```python
from typing import TypedDict, Literal
from cloudevents_pydantic.events import CloudEvent, field_types
from typing import Annotated, Literal, TypedDict

from pydantic import Field

from cloudevents_pydantic.events import CloudEvent
from cloudevents_pydantic.events.fields import metadata, types


class OrderCreatedData(TypedDict):
a_str: field_types.String
an_int: field_types.Integer
a_str: types.String
an_int: types.Integer

class OrderCreated(CloudEvent):
data: OrderCreatedData
type: Literal["order_created"] = "order_created"
source: field_types.String = "order_service"

event = OrderCreated.event_factory(
data={"a_str": "a nice string", "an_int": 1},
OrderCreatedDataField = Field(
title="An order representation",
description="A nice new order has been created! OMG!",
examples=["{'a_str': 'a nice string', 'an_int': 1}"],
)
```
///

/// tab | inline Syntax
```python
from typing import TypedDict, Literal
from cloudevents_pydantic.events import CloudEvent, field_types

class OrderCreated(CloudEvent):
data: TypedDict("OrderCreatedData", {"a_str": field_types.String, "an_int": field_types.Integer})
type: Literal["order_created"] = "order_created"
source: field_types.String = "order_service"

event = OrderCreated.event_factory(
data={"a_str": "a nice string", "an_int": 1},
)
data: Annotated[OrderCreatedData, OrderCreatedDataField]
type: Annotated[
Literal["order_created"], Field(default="order_created"), metadata.FieldType
]
source: Annotated[
Literal["order_service"], Field(default="order_service"), metadata.FieldSource
]
```
///


/// admonition | Use subclasses
type: warning
Expand Down
2 changes: 1 addition & 1 deletion tests/events/test__event.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from ulid import ULID

from cloudevents_pydantic.events import CloudEvent
from cloudevents_pydantic.events.field_types import SpecVersion
from cloudevents_pydantic.events.fields.types import SpecVersion

test_attributes = {
"type": "com.example.string",
Expand Down
2 changes: 1 addition & 1 deletion tests/events/test_field_types_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import pytest
from pydantic import BaseModel

from cloudevents_pydantic.events.field_types import (
from cloudevents_pydantic.events.fields.types import (
Binary,
Boolean,
URIReference,
Expand Down
2 changes: 1 addition & 1 deletion tests/events/test_field_types_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from typing_extensions import TypedDict

from cloudevents_pydantic.events import CloudEvent
from cloudevents_pydantic.events.field_types import (
from cloudevents_pydantic.events.fields.types import (
URI,
Binary,
Integer,
Expand Down
2 changes: 1 addition & 1 deletion tests/formats/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from typing_extensions import TypedDict

from cloudevents_pydantic.events import CloudEvent
from cloudevents_pydantic.events.field_types import Binary, SpecVersion
from cloudevents_pydantic.events.fields.types import Binary, SpecVersion
from cloudevents_pydantic.formats.json import (
from_json,
from_json_batch,
Expand Down

0 comments on commit b32c291

Please sign in to comment.