Skip to content

Commit

Permalink
add pydantic serializer
Browse files Browse the repository at this point in the history
  • Loading branch information
zerlok committed Dec 21, 2024
1 parent 9b52b31 commit d016294
Show file tree
Hide file tree
Showing 6 changed files with 655 additions and 334 deletions.
847 changes: 514 additions & 333 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "BrokRPC"
version = "0.2.3"
version = "0.2.4"
description = "framework for gRPC like server-client communication over message brokers"
authors = ["zerlok <[email protected]>"]
readme = "README.md"
Expand Down Expand Up @@ -33,11 +33,13 @@ protobuf = {version = "^5.26.1", optional = true}
googleapis-common-protos = {version = "^1.65.0", optional = true}
aiormq = {version = "^6.8.1", optional = true}
aiofiles = {version = "^24.1.0", optional = true}
pydantic = {version = "^2.10.4", optional = true}

[tool.poetry.extras]
cli = ["aiofiles"]
protobuf = ["protobuf", "googleapis-common-protos"]
aiormq = ["aiormq"]
pydantic = ["pydantic"]

[tool.poetry.scripts]
brokrpc = "brokrpc.cli:main"
Expand Down
74 changes: 74 additions & 0 deletions src/brokrpc/serializer/pydantic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from pydantic import BaseModel
from pydantic.main import IncEx
from pydantic_core import PydanticSerializationError

from brokrpc.abc import Serializer
from brokrpc.message import BinaryMessage, Message, PackedMessage, UnpackedMessage
from brokrpc.model import SerializerDumpError, SerializerLoadError


class PydanticSerializer[T: BaseModel](Serializer[Message[T], BinaryMessage]):
# NOTE: allow `model_dump_json` & `model_validate_json` pydantic model methods customization
def __init__( # noqa: PLR0913
self,
model: type[T],
*,
indent: int | None = None,
include: IncEx | None = None,
exclude: IncEx | None = None,
context: object = None,
by_alias: bool = True,
exclude_unset: bool = True,
exclude_defaults: bool = False,
exclude_none: bool = True,
strict: bool | None = False,
) -> None:
self.__model = model
self.__indent = indent
self.__include = include
self.__exclude = exclude
self.__context = context
self.__by_alias = by_alias
self.__exclude_unset = exclude_unset
self.__exclude_defaults = exclude_defaults
self.__exclude_none = exclude_none
self.__strict = strict

def dump_message(self, message: Message[T]) -> BinaryMessage:
assert isinstance(message.body, self.__model)

try:
body = message.body.model_dump_json(
indent=self.__indent,
include=self.__include,
exclude=self.__exclude,
context=self.__context,
by_alias=self.__by_alias,
exclude_unset=self.__exclude_unset,
exclude_defaults=self.__exclude_defaults,
exclude_none=self.__exclude_none,
).encode()

except PydanticSerializationError as err:
details = "can't dump pydantic model"
raise SerializerDumpError(details, message) from err

return PackedMessage(
body=body,
content_type="application/json",
content_encoding="utf-8",
original=message,
)

def load_message(self, message: BinaryMessage) -> Message[T]:
try:
body = self.__model.model_validate_json(message.body, strict=self.__strict)

except ValueError as err:
details = "can't load pydantic model"
raise SerializerLoadError(details, message) from err

return UnpackedMessage(
original=message,
body=body,
)
29 changes: 29 additions & 0 deletions tests/integration/test_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import pytest
from pydantic import BaseModel

from brokrpc.message import AppMessage, Message
from brokrpc.serializer.json import JSONSerializer
from brokrpc.serializer.pydantic import PydanticSerializer
from tests.stub.pydantic import FooModel


@pytest.mark.parametrize("obj", [FooModel(num=42, s="spam", bar=FooModel.Bar(str2int={"eggs": 59}))])
def test_pydantic_dump_json_load_ok(
pydantic_serializer: PydanticSerializer,
json_serializer: JSONSerializer,
message: Message[object],
obj: BaseModel,
) -> None:
loaded = json_serializer.load_message(pydantic_serializer.dump_message(message))

assert loaded.body == obj.model_dump(mode="json", by_alias=True, exclude_unset=True, exclude_none=True)


@pytest.fixture
def pydantic_serializer(obj: BaseModel) -> PydanticSerializer:
return PydanticSerializer(type(obj))


@pytest.fixture
def message(obj: object, stub_routing_key: str) -> Message[object]:
return AppMessage(body=obj, routing_key=stub_routing_key)
12 changes: 12 additions & 0 deletions tests/stub/pydantic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import typing as t

from pydantic import BaseModel


class FooModel(BaseModel):
class Bar(BaseModel):
str2int: t.Mapping[str, int]

num: int
s: str
bar: Bar
23 changes: 23 additions & 0 deletions tests/unit/serializer/test_pydantic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import pytest
from pydantic import BaseModel

from brokrpc.message import AppMessage, Message
from brokrpc.serializer.pydantic import PydanticSerializer
from tests.stub.pydantic import FooModel


@pytest.mark.parametrize("obj", [FooModel(num=42, s="spam", bar=FooModel.Bar(str2int={"eggs": 59}))])
def test_dump_load_ok(serializer: PydanticSerializer, message: Message[object]) -> None:
loaded = serializer.load_message(serializer.dump_message(message))

assert loaded.body == message.body


@pytest.fixture
def serializer(obj: BaseModel) -> PydanticSerializer:
return PydanticSerializer(type(obj))


@pytest.fixture
def message(obj: object, stub_routing_key: str) -> Message[object]:
return AppMessage(body=obj, routing_key=stub_routing_key)

0 comments on commit d016294

Please sign in to comment.