Skip to content

Commit

Permalink
Introduce Pydantic-based message classes for improved validation and …
Browse files Browse the repository at this point in the history
…serialization
  • Loading branch information
grongierisc committed Jan 22, 2025
1 parent 5441f73 commit aaadd6c
Show file tree
Hide file tree
Showing 10 changed files with 61 additions and 37 deletions.
14 changes: 14 additions & 0 deletions docs/python-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ class MyRequest(Message):
request_string: str = None
```

### PydanticMessage 📦

Base class for messages that use Pydantic models for validation and serialization. This class provides additional features for data validation and serialization.

**Usage:** Subclass `PydanticMessage` and define a Pydantic model as a class attribute. This approach provides automatic validation and serialization.

**Example:**
```python
from iop import PydanticMessage

class MyRequest(PydanticMessage):
model : str = None
```

### BusinessService 🔄
Base class for business services that receive and process incoming data. Business services act as entry points for data into your interoperability solution.

Expand Down
2 changes: 1 addition & 1 deletion src/grongier/pex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from iop._inbound_adapter import _InboundAdapter
from iop._outbound_adapter import _OutboundAdapter
from iop._message import _Message
from iop._pickle_message import _PickleMessage
from iop._message import _PickleMessage
from iop._director import _Director
from iop._utils import _Utils

Expand Down
5 changes: 2 additions & 3 deletions src/iop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
from iop._business_service import _BusinessService
from iop._director import _Director
from iop._inbound_adapter import _InboundAdapter
from iop._message import _Message
from iop._pydantic_message import _PydanticMessage
from iop._message import _Message, _PickleMessage, _PydanticMessage, _PydanticPickleMessage
from iop._outbound_adapter import _OutboundAdapter
from iop._pickle_message import _PickleMessage
from iop._private_session_duplex import _PrivateSessionDuplex
from iop._private_session_process import _PrivateSessionProcess
from iop._utils import _Utils
Expand All @@ -23,4 +21,5 @@ class DuplexProcess(_PrivateSessionProcess): pass
class Message(_Message): pass
class PickleMessage(_PickleMessage): pass
class PydanticMessage(_PydanticMessage): pass
class PydanticPickleMessage(_PydanticPickleMessage): pass
class Director(_Director): pass
24 changes: 23 additions & 1 deletion src/iop/_message.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
from typing import Any
from pydantic import BaseModel

class _Message:
""" The abstract class that is the superclass for persistent messages sent from one component to another.
This class has no properties or methods. Users subclass Message and add properties.
The IOP framework provides the persistence to objects derived from the Message class.
"""
pass
pass

class _PickleMessage:
""" The abstract class that is the superclass for persistent messages sent from one component to another.
This class has no properties or methods. Users subclass Message and add properties.
The IOP framework provides the persistence to objects derived from the Message class.
"""
pass

class _PydanticMessage(BaseModel):
"""Base class for Pydantic-based messages that can be serialized to IRIS."""

def __init__(self, **data: Any):
super().__init__(**data)

class _PydanticPickleMessage(BaseModel):
"""Base class for Pydantic-based messages that can be serialized to IRIS."""

def __init__(self, **data: Any):
super().__init__(**data)
14 changes: 7 additions & 7 deletions src/iop/_message_validator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import dataclasses
from typing import Any, Type
from pydantic import BaseModel
from iop._message import _Message
from iop._message import _Message, _PickleMessage, _PydanticPickleMessage, BaseModel


def is_message_instance(obj: Any) -> bool:
Expand All @@ -17,6 +16,8 @@ def is_message_instance(obj: Any) -> bool:

def is_pickle_message_instance(obj: Any) -> bool:
"""Check if object is a PickleMessage instance."""
if isinstance(obj, _PydanticPickleMessage):
return True
if is_pickle_message_class(type(obj)):
return True
return False
Expand All @@ -31,8 +32,6 @@ def is_iris_object_instance(obj: Any) -> bool:

def is_message_class(klass: Type) -> bool:
"""Check if class is a Message type."""
if issubclass(klass, BaseModel):
return True
if issubclass(klass, _Message):
return True
return False
Expand All @@ -41,7 +40,8 @@ def is_message_class(klass: Type) -> bool:

def is_pickle_message_class(klass: Type) -> bool:
"""Check if class is a PickleMessage type."""
name = f"{klass.__module__}.{klass.__qualname__}"
if name in ("iop.PickleMessage", "grongier.pex.PickleMessage"):
if issubclass(klass, _PickleMessage):
return True
return any(is_pickle_message_class(c) for c in klass.__bases__)
if issubclass(klass, _PydanticPickleMessage):
return True
return False
6 changes: 0 additions & 6 deletions src/iop/_pickle_message.py

This file was deleted.

9 changes: 0 additions & 9 deletions src/iop/_pydantic_message.py

This file was deleted.

4 changes: 4 additions & 0 deletions src/iop/_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from dacite import Config, from_dict
import iris

from iop._message import _PydanticPickleMessage
from iop._utils import _Utils
from pydantic import BaseModel

Expand Down Expand Up @@ -137,6 +138,9 @@ class MessageSerializer:
@staticmethod
def serialize(message: Any, use_pickle: bool = False) -> iris.cls:
"""Serializes a message to IRIS format."""
# Check for PydanticPickleMessage first
if isinstance(message, _PydanticPickleMessage):
return MessageSerializer._serialize_pickle(message)
if isinstance(message, BaseModel):
return (MessageSerializer._serialize_pickle(message)
if use_pickle else MessageSerializer._serialize_json(message))
Expand Down
18 changes: 9 additions & 9 deletions src/tests/test_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
dataclass_from_dict
)
from iop._message import _Message as Message
from iop._pydantic_message import _PydanticMessage as PydanticMessage
from iop._message import _PydanticMessage as PydanticMessage

class SimpleModel(PydanticMessage):
text: str
Expand All @@ -32,7 +32,7 @@ class ComplexModel(PydanticMessage):


@dataclass
class TestMessage(Message):
class MessageTest(Message):
text: str
number: int

Expand Down Expand Up @@ -62,17 +62,17 @@ def test_simple_message_serialization():
assert result.number == msg.number

def test_message_serialization():
msg = TestMessage(text="test", number=42)
msg = MessageTest(text="test", number=42)

# Test serialization
serial = serialize_message(msg)
assert type(serial).__module__.startswith('iris')
assert serial._IsA("IOP.Message")
assert serial.classname == f"{TestMessage.__module__}.{TestMessage.__name__}"
assert serial.classname == f"{MessageTest.__module__}.{MessageTest.__name__}"

# Test deserialization
result = deserialize_message(serial)
assert isinstance(result, TestMessage)
assert isinstance(result, MessageTest)
assert result.text == msg.text
assert result.number == msg.number

Expand All @@ -91,7 +91,7 @@ def test_pickle_message_serialization():
assert result.number == msg.number

def test_pickle_serialization():
msg = TestMessage(text="test", number=42)
msg = MessageTest(text="test", number=42)

# Test serialization
serial = serialize_pickle_message(msg)
Expand All @@ -100,7 +100,7 @@ def test_pickle_serialization():

# Test deserialization
result = deserialize_pickle_message(serial)
assert isinstance(result, TestMessage)
assert isinstance(result, MessageTest)
assert result.text == msg.text
assert result.number == msg.number

Expand Down Expand Up @@ -165,8 +165,8 @@ def test_dataclass_from_dict():
'extra_field': 'extra'
}

result = dataclass_from_dict(TestMessage, data)
assert isinstance(result, TestMessage)
result = dataclass_from_dict(MessageTest, data)
assert isinstance(result, MessageTest)
assert result.text == 'test'
assert result.number == 42
assert result.extra_field == 'extra'
Expand Down
2 changes: 1 addition & 1 deletion src/tests/test_pydantic_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest
from pydantic import BaseModel

from iop._pydantic_message import _PydanticMessage as PydanticMessage
from iop._message import _PydanticMessage as PydanticMessage
from iop._serialization import (
serialize_message,
deserialize_message,
Expand Down

0 comments on commit aaadd6c

Please sign in to comment.