Skip to content

Commit 5441f73

Browse files
committed
Add Pydantic support for message handling and update dependencies
1 parent 84193ae commit 5441f73

File tree

11 files changed

+402
-19
lines changed

11 files changed

+402
-19
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ requires = ["setuptools", "wheel"]
33

44
[project]
55
name = "iris_pex_embedded_python"
6-
version = "3.3.2"
6+
version = "3.4.0"
77
description = "Iris Interoperability based on Embedded Python"
88
readme = "README.md"
99
authors = [
@@ -28,6 +28,7 @@ classifiers = [
2828

2929
dependencies = [
3030
"dacite >=1.6.0",
31+
"pydantic>=2.0.0",
3132
"xmltodict>=0.12.0",
3233
"iris-embedded-python-wrapper>=0.0.6",
3334
"setuptools>=40.8.0",

requirements-dev.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ wheel
99
twine
1010
iris-embedded-python-wrapper
1111
dc-schema
12-
jsonpath-ng
12+
jsonpath-ng
13+
pydantic>=2.0.0

src/iop/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from iop._director import _Director
55
from iop._inbound_adapter import _InboundAdapter
66
from iop._message import _Message
7+
from iop._pydantic_message import _PydanticMessage
78
from iop._outbound_adapter import _OutboundAdapter
89
from iop._pickle_message import _PickleMessage
910
from iop._private_session_duplex import _PrivateSessionDuplex
@@ -21,4 +22,5 @@ class DuplexOperation(_PrivateSessionDuplex): pass
2122
class DuplexProcess(_PrivateSessionProcess): pass
2223
class Message(_Message): pass
2324
class PickleMessage(_PickleMessage): pass
25+
class PydanticMessage(_PydanticMessage): pass
2426
class Director(_Director): pass

src/iop/_dispatch.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import iris
99
from dacite import Config, from_dict
10+
from pydantic import BaseModel
1011

1112
from iop._utils import _Utils
1213
from iop._serialization import IrisJSONEncoder, IrisJSONDecoder
@@ -165,17 +166,20 @@ def dataclass_from_dict(klass: Type, dikt: Dict) -> Any:
165166
Returns:
166167
A dataclass object with the fields of the dataclass and the fields of the dictionary.
167168
"""
168-
ret = from_dict(klass, dikt, Config(check_types=False))
169-
170-
try:
171-
fieldtypes = klass.__annotations__
172-
except Exception as e:
173-
fieldtypes = []
174-
175-
for key, val in dikt.items():
176-
if key not in fieldtypes:
177-
setattr(ret, key, val)
178-
return ret
169+
if issubclass(klass, BaseModel):
170+
return klass.model_validate(dikt)
171+
else:
172+
ret = from_dict(klass, dikt, Config(check_types=False))
173+
174+
try:
175+
fieldtypes = klass.__annotations__
176+
except Exception as e:
177+
fieldtypes = []
178+
179+
for key, val in dikt.items():
180+
if key not in fieldtypes:
181+
setattr(ret, key, val)
182+
return ret
179183

180184
def dispach_message(host, request: Any) -> Any:
181185
"""Dispatches the message to the appropriate method.

src/iop/_message_validator.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import dataclasses
22
from typing import Any, Type
3+
from pydantic import BaseModel
4+
from iop._message import _Message
35

46

57
def is_message_instance(obj: Any) -> bool:
68
"""Check if object is a valid Message instance."""
9+
if isinstance(obj, BaseModel):
10+
return True
711
if is_message_class(type(obj)):
812
if not dataclasses.is_dataclass(obj):
913
raise TypeError(f"{type(obj).__module__}.{type(obj).__qualname__} must be a dataclass")
@@ -27,10 +31,12 @@ def is_iris_object_instance(obj: Any) -> bool:
2731

2832
def is_message_class(klass: Type) -> bool:
2933
"""Check if class is a Message type."""
30-
name = f"{klass.__module__}.{klass.__qualname__}"
31-
if name in ("iop.Message", "grongier.pex.Message"):
34+
if issubclass(klass, BaseModel):
35+
return True
36+
if issubclass(klass, _Message):
3237
return True
33-
return any(is_message_class(c) for c in klass.__bases__)
38+
return False
39+
3440

3541

3642
def is_pickle_message_class(klass: Type) -> bool:

src/iop/_pydantic_message.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
from typing import Any
3+
from pydantic import BaseModel
4+
5+
class _PydanticMessage(BaseModel):
6+
"""Base class for Pydantic-based messages that can be serialized to IRIS."""
7+
8+
def __init__(self, **data: Any):
9+
super().__init__(**data)

src/iop/_serialization.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import iris
1616

1717
from iop._utils import _Utils
18+
from pydantic import BaseModel
1819

1920
# Constants
2021
DATETIME_FORMAT_LENGTH = 23
@@ -91,6 +92,8 @@ class IrisJSONEncoder(json.JSONEncoder):
9192
"""JSONEncoder that handles dates, decimals, UUIDs, etc."""
9293

9394
def default(self, obj: Any) -> Any:
95+
if isinstance(obj, BaseModel):
96+
return obj.model_dump()
9497
if obj.__class__.__name__ == 'DataFrame':
9598
return f'dataframe:{TypeConverter.convert_to_string("dataframe", obj)}'
9699
elif isinstance(obj, datetime.datetime):
@@ -134,6 +137,9 @@ class MessageSerializer:
134137
@staticmethod
135138
def serialize(message: Any, use_pickle: bool = False) -> iris.cls:
136139
"""Serializes a message to IRIS format."""
140+
if isinstance(message, BaseModel):
141+
return (MessageSerializer._serialize_pickle(message)
142+
if use_pickle else MessageSerializer._serialize_json(message))
137143
if use_pickle:
138144
return MessageSerializer._serialize_pickle(message)
139145
return MessageSerializer._serialize_json(message)
@@ -155,7 +161,11 @@ def _serialize_pickle(message: Any) -> iris.cls:
155161

156162
@staticmethod
157163
def _serialize_json(message: Any) -> iris.cls:
158-
json_string = json.dumps(message, cls=IrisJSONEncoder, ensure_ascii=False)
164+
if isinstance(message, BaseModel):
165+
json_string = json.dumps(message.model_dump(), cls=IrisJSONEncoder, ensure_ascii=False)
166+
else:
167+
json_string = json.dumps(message, cls=IrisJSONEncoder, ensure_ascii=False)
168+
159169
msg = iris.cls('IOP.Message')._New()
160170
msg.classname = f"{message.__class__.__module__}.{message.__class__.__name__}"
161171

@@ -187,7 +197,10 @@ def _deserialize_json(serial: iris.cls) -> Any:
187197

188198
try:
189199
json_dict = json.loads(json_string, cls=IrisJSONDecoder)
190-
return dataclass_from_dict(msg_class, json_dict)
200+
if issubclass(msg_class, BaseModel):
201+
return msg_class.model_validate(json_dict)
202+
else:
203+
return dataclass_from_dict(msg_class, json_dict)
191204
except Exception as e:
192205
raise SerializationError(f"Failed to deserialize JSON: {str(e)}")
193206

src/tests/bench/msg.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
from grongier.pex import Message
1+
from iop import PydanticMessage
2+
from iop import Message
23
from dataclasses import dataclass
34

45
@dataclass
56
class MyMessage(Message):
7+
message : str = None
8+
9+
class MyPydanticMessage(PydanticMessage):
610
message : str = None

src/tests/test_bench.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ class TestBenchIoP:
2929
'message_type': 'msg.MyMessage',
3030
'use_json': True
3131
},
32+
{
33+
'name': 'Python BP to Python BO with Python Pydantic Message',
34+
'component': 'Python.BenchIoPProcess',
35+
'message_type': 'msg.MyPydanticMessage',
36+
'use_json': True
37+
},
38+
{
39+
'name': 'Python BP to ObjetScript BO with Python Pydantic Message',
40+
'component': 'Python.BenchIoPProcess.To.Cls',
41+
'message_type': 'msg.MyPydanticMessage',
42+
'use_json': True
43+
},
3244
{
3345
'name': 'ObjetScript BP to Python BO with Iris Message',
3446
'component': 'Bench.Process',
@@ -52,6 +64,18 @@ class TestBenchIoP:
5264
'component': 'Bench.Process.To.Cls',
5365
'message_type': 'msg.MyMessage',
5466
'use_json': True
67+
},
68+
{
69+
'name': 'ObjetScript BP to Python BO with Python Pydantic Message',
70+
'component': 'Bench.Process',
71+
'message_type': 'msg.MyPydanticMessage',
72+
'use_json': True
73+
},
74+
{
75+
'name': 'ObjetScript BP to ObjetScript BO with Python Pydantic Message',
76+
'component': 'Bench.Process.To.Cls',
77+
'message_type': 'msg.MyPydanticMessage',
78+
'use_json': True
5579
}
5680
]
5781

0 commit comments

Comments
 (0)