iris-pex-embedded-python 3.7.2b2__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iop/__init__.py +276 -18
- iop/__main__.py +3 -3
- iop/cli/__init__.py +1 -0
- iop/cli/formatting.py +45 -0
- iop/{_cli.py → cli/main.py} +238 -216
- iop/cli/parser.py +165 -0
- iop/cli/types.py +73 -0
- iop/cls/IOP/Common.cls +11 -2
- iop/cls/IOP/PrivateSession/Duplex.cls +1 -1
- iop/cls/IOP/Service/Remote/Rest/v1.cls +152 -5
- iop/cls/IOP/Utils.cls +814 -7
- iop/cls/IOP/Wrapper.cls +1 -1
- iop/components/__init__.py +16 -0
- iop/{_async_request.py → components/async_request.py} +7 -7
- iop/components/business_host.py +363 -0
- iop/components/business_operation.py +84 -0
- iop/{_business_process.py → components/business_process.py} +145 -125
- iop/components/business_service.py +106 -0
- iop/{_common.py → components/common.py} +160 -164
- iop/{_debugpy.py → components/debugpy.py} +11 -11
- iop/{_generator_request.py → components/generator_request.py} +4 -4
- iop/{_inbound_adapter.py → components/inbound_adapter.py} +10 -18
- iop/{_log_manager.py → components/log_manager.py} +1 -1
- iop/{_outbound_adapter.py → components/outbound_adapter.py} +1 -1
- iop/components/polling_business_service.py +48 -0
- iop/{_private_session_duplex.py → components/private_session_duplex.py} +7 -9
- iop/{_private_session_process.py → components/private_session_process.py} +2 -2
- iop/{_settings.py → components/settings.py} +23 -3
- iop/messages/__init__.py +7 -0
- iop/messages/base.py +61 -0
- iop/{_decorators.py → messages/decorators.py} +8 -2
- iop/messages/dispatch.py +351 -0
- iop/{_persistent_message.py → messages/persistent.py} +24 -19
- iop/{_serialization.py → messages/serialization.py} +34 -21
- iop/{_message_validator.py → messages/validation.py} +15 -13
- iop/migration/__init__.py +1 -0
- iop/migration/io.py +61 -0
- iop/migration/manifest.py +534 -0
- iop/migration/plans.py +80 -0
- iop/migration/utils.py +1126 -0
- iop/production/__init__.py +55 -0
- iop/production/actions.py +525 -0
- iop/production/common.py +103 -0
- iop/production/component.py +234 -0
- iop/production/declarations.py +190 -0
- iop/production/declarative.py +214 -0
- iop/production/diff.py +327 -0
- iop/production/import_.py +296 -0
- iop/production/inspection.py +92 -0
- iop/production/model.py +1143 -0
- iop/production/planning.py +678 -0
- iop/production/reconstruction.py +341 -0
- iop/production/rendering.py +610 -0
- iop/production/runtime.py +91 -0
- iop/production/source_inference.py +729 -0
- iop/production/types.py +546 -0
- iop/production/validation.py +452 -0
- iop/runtime/__init__.py +1 -0
- iop/runtime/director.py +415 -0
- iop/runtime/environment.py +59 -0
- iop/{_iris.py → runtime/iris.py} +2 -2
- iop/runtime/local.py +145 -0
- iop/{_director_protocol.py → runtime/protocol.py} +25 -8
- iop/runtime/remote/__init__.py +12 -0
- iop/runtime/remote/client.py +90 -0
- iop/runtime/remote/director.py +283 -0
- iop/runtime/remote/migration.py +57 -0
- iop/runtime/remote/settings.py +69 -0
- iop/runtime/remote/setup.py +72 -0
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/METADATA +11 -2
- iris_pex_embedded_python-4.0.0.dist-info/RECORD +99 -0
- iris_pex_embedded_python-4.0.0.dist-info/entry_points.txt +2 -0
- iris_pex_embedded_python-4.0.0.dist-info/top_level.txt +1 -0
- grongier/__init__.py +0 -0
- grongier/cls/Grongier/PEX/BusinessOperation.cls +0 -8
- grongier/cls/Grongier/PEX/BusinessProcess.cls +0 -13
- grongier/cls/Grongier/PEX/BusinessService.cls +0 -8
- grongier/cls/Grongier/PEX/Common.cls +0 -10
- grongier/cls/Grongier/PEX/Director.cls +0 -10
- grongier/cls/Grongier/PEX/Duplex/Operation.cls +0 -4
- grongier/cls/Grongier/PEX/Duplex/Process.cls +0 -13
- grongier/cls/Grongier/PEX/Duplex/Service.cls +0 -4
- grongier/cls/Grongier/PEX/InboundAdapter.cls +0 -8
- grongier/cls/Grongier/PEX/Message.cls +0 -13
- grongier/cls/Grongier/PEX/OutboundAdapter.cls +0 -8
- grongier/cls/Grongier/PEX/PickleMessage.cls +0 -13
- grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +0 -8
- grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +0 -14
- grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +0 -14
- grongier/cls/Grongier/PEX/Test.cls +0 -10
- grongier/cls/Grongier/PEX/Utils.cls +0 -10
- grongier/cls/Grongier/Service/WSGI.cls +0 -4
- grongier/pex/__init__.py +0 -75
- grongier/pex/__main__.py +0 -4
- grongier/pex/_business_host.py +0 -1
- grongier/pex/_cli.py +0 -4
- grongier/pex/_common.py +0 -1
- grongier/pex/_director.py +0 -1
- grongier/pex/_utils.py +0 -1
- grongier/pex/wsgi/handlers.py +0 -104
- iop/_business_host.py +0 -322
- iop/_business_operation.py +0 -77
- iop/_business_service.py +0 -67
- iop/_director.py +0 -333
- iop/_dispatch.py +0 -194
- iop/_local.py +0 -106
- iop/_message.py +0 -45
- iop/_polling_business_service.py +0 -6
- iop/_remote.py +0 -473
- iop/_utils.py +0 -912
- iop/cls/IOP/Service/WSGI.cls +0 -310
- iop/wsgi/handlers.py +0 -112
- iris_pex_embedded_python-3.7.2b2.dist-info/RECORD +0 -96
- iris_pex_embedded_python-3.7.2b2.dist-info/entry_points.txt +0 -2
- iris_pex_embedded_python-3.7.2b2.dist-info/top_level.txt +0 -2
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/WHEEL +0 -0
- {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import importlib
|
|
2
2
|
|
|
3
|
-
from .
|
|
4
|
-
from ._decorators import (
|
|
3
|
+
from ..messages.decorators import (
|
|
5
4
|
input_deserializer,
|
|
6
5
|
input_serializer_param,
|
|
7
|
-
output_serializer,
|
|
8
6
|
output_deserializer,
|
|
7
|
+
output_serializer,
|
|
9
8
|
)
|
|
10
|
-
from .
|
|
9
|
+
from ..messages.dispatch import dispatch_message
|
|
10
|
+
from .business_host import _BusinessHost
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class _PrivateSessionDuplex(_BusinessHost):
|
|
@@ -19,7 +19,7 @@ class _PrivateSessionDuplex(_BusinessHost):
|
|
|
19
19
|
"""Called when the business operation receives a message from another production component.
|
|
20
20
|
Typically, the operation will either send the message to the external system or forward it to a business process or another business operation.
|
|
21
21
|
If the operation has an adapter, it uses the Adapter.invoke() method to call the method on the adapter that sends the message to the external system.
|
|
22
|
-
If the operation is forwarding the message to another production component, it uses
|
|
22
|
+
If the operation is forwarding the message to another production component, it uses send_request_async() or send_request_sync().
|
|
23
23
|
|
|
24
24
|
Parameters:
|
|
25
25
|
request: An instance of either a subclass of Message or of IRISObject containing the incoming message for the business operation.
|
|
@@ -33,16 +33,14 @@ class _PrivateSessionDuplex(_BusinessHost):
|
|
|
33
33
|
@output_serializer
|
|
34
34
|
def _dispatch_on_message(self, request):
|
|
35
35
|
"""For internal use only."""
|
|
36
|
-
return
|
|
36
|
+
return dispatch_message(self, request)
|
|
37
37
|
|
|
38
38
|
def _set_iris_handles(self, handle_current, handle_partner):
|
|
39
39
|
"""For internal use only."""
|
|
40
40
|
self.iris_handle = handle_current
|
|
41
41
|
if type(handle_partner).__module__.find("iris") == 0:
|
|
42
42
|
if (
|
|
43
|
-
handle_partner._IsA("
|
|
44
|
-
or handle_partner._IsA("Grongier.PEX.OutboundAdapter")
|
|
45
|
-
or handle_partner._IsA("IOP.InboundAdapter")
|
|
43
|
+
handle_partner._IsA("IOP.InboundAdapter")
|
|
46
44
|
or handle_partner._IsA("IOP.OutboundAdapter")
|
|
47
45
|
):
|
|
48
46
|
module = importlib.import_module(handle_partner.GetModule())
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from .
|
|
2
|
-
from .
|
|
1
|
+
from ..messages.decorators import input_deserializer, output_serializer
|
|
2
|
+
from .business_process import _BusinessProcess
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class _PrivateSessionProcess(_BusinessProcess):
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
from
|
|
2
|
-
from typing import Any
|
|
1
|
+
from __future__ import annotations
|
|
3
2
|
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, overload
|
|
4
5
|
|
|
5
6
|
_MISSING = object()
|
|
6
7
|
|
|
@@ -23,7 +24,13 @@ def _string_value(value: Any) -> str:
|
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class Setting:
|
|
26
|
-
"""Metadata for an IRIS production setting.
|
|
27
|
+
"""Metadata for an IRIS production setting.
|
|
28
|
+
|
|
29
|
+
Use Setting or setting(...) for configurable component values exposed in
|
|
30
|
+
the Management Portal. Use iop.target() instead for outbound component
|
|
31
|
+
routes. See docs/cookbooks/production-settings-and-targets.md and
|
|
32
|
+
docs/settings.md.
|
|
33
|
+
"""
|
|
27
34
|
|
|
28
35
|
def __init__(
|
|
29
36
|
self,
|
|
@@ -46,9 +53,17 @@ class Setting:
|
|
|
46
53
|
self.control = control or ""
|
|
47
54
|
self.exclude = exclude
|
|
48
55
|
self.name = ""
|
|
56
|
+
self.owner = None
|
|
49
57
|
|
|
50
58
|
def __set_name__(self, owner, name):
|
|
51
59
|
self.name = name
|
|
60
|
+
self.owner = owner
|
|
61
|
+
|
|
62
|
+
@overload
|
|
63
|
+
def __get__(self, instance: None, owner: type | None = None) -> Setting: ...
|
|
64
|
+
|
|
65
|
+
@overload
|
|
66
|
+
def __get__(self, instance: object, owner: type | None = None) -> Any: ...
|
|
52
67
|
|
|
53
68
|
def __get__(self, instance, owner=None):
|
|
54
69
|
if instance is None:
|
|
@@ -68,6 +83,11 @@ class Setting:
|
|
|
68
83
|
|
|
69
84
|
|
|
70
85
|
def setting(default: Any = _MISSING, **kwargs) -> Setting:
|
|
86
|
+
"""Declare a configurable IRIS production setting on a component class.
|
|
87
|
+
|
|
88
|
+
For outbound routing, prefer iop.target() so Production.connect(...) can
|
|
89
|
+
wire the graph explicitly. See docs/cookbooks/production-settings-and-targets.md.
|
|
90
|
+
"""
|
|
71
91
|
return Setting(default, **kwargs)
|
|
72
92
|
|
|
73
93
|
|
iop/messages/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .base import _Message as _Message
|
|
2
|
+
from .base import _PickleMessage as _PickleMessage
|
|
3
|
+
from .base import _PydanticMessage as _PydanticMessage
|
|
4
|
+
from .base import _PydanticPickleMessage as _PydanticPickleMessage
|
|
5
|
+
from .persistent import Field as Field
|
|
6
|
+
from .persistent import Model as Model
|
|
7
|
+
from .persistent import _PersistentMessage as _PersistentMessage
|
iop/messages/base.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class _Message:
|
|
7
|
+
"""Base class for JSON-serialized messages sent between components.
|
|
8
|
+
|
|
9
|
+
Use Message for Python-only contracts that do not need native IRIS
|
|
10
|
+
persistence. Subclasses must be decorated with @dataclass. App builders
|
|
11
|
+
usually pair message classes with BusinessProcess or BusinessOperation
|
|
12
|
+
handlers; see
|
|
13
|
+
docs/cookbooks/add-business-process.md and
|
|
14
|
+
docs/cookbooks/add-business-operation.md.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
_iris_id: str | None = None
|
|
18
|
+
|
|
19
|
+
def get_iris_id(self) -> str | None:
|
|
20
|
+
"""Get the IRIS ID of the message."""
|
|
21
|
+
return self._iris_id
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _PickleMessage(_Message):
|
|
25
|
+
"""Base class for pickle-serialized messages sent between components.
|
|
26
|
+
|
|
27
|
+
Prefer Message or PydanticMessage for new application contracts. Use
|
|
28
|
+
PickleMessage only when a Python-only payload cannot be represented cleanly
|
|
29
|
+
as JSON-compatible fields.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _PydanticMessage(BaseModel):
|
|
36
|
+
"""Base class for Pydantic-based messages that can be serialized to IRIS.
|
|
37
|
+
|
|
38
|
+
Use PydanticMessage for Python-only contracts that benefit from Pydantic
|
|
39
|
+
validation. Do not decorate PydanticMessage classes with @dataclass and do
|
|
40
|
+
not register these classes in CLASSES; see docs/getting-started/register-component.md.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
_iris_id: str | None = None
|
|
44
|
+
|
|
45
|
+
def __init__(self, **data: Any):
|
|
46
|
+
super().__init__(**data)
|
|
47
|
+
|
|
48
|
+
def get_iris_id(self) -> str | None:
|
|
49
|
+
"""Get the IRIS ID of the message."""
|
|
50
|
+
return self._iris_id
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _PydanticPickleMessage(_PydanticMessage):
|
|
54
|
+
"""Base class for Pydantic-based messages serialized through pickle.
|
|
55
|
+
|
|
56
|
+
Prefer PydanticMessage unless the payload must preserve Python-only object
|
|
57
|
+
shapes that JSON serialization cannot represent.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, **data: Any):
|
|
61
|
+
super().__init__(**data)
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
1
2
|
from functools import wraps
|
|
2
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
|
-
from .
|
|
5
|
+
from .dispatch import dispatch_deserializer, dispatch_serializer
|
|
6
|
+
from .dispatch import handler as handler
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
def input_serializer(fonction: Callable) -> Callable:
|
|
8
10
|
"""Decorator that serializes all input arguments."""
|
|
9
11
|
|
|
12
|
+
@wraps(fonction)
|
|
10
13
|
def _dispatch_serializer(self, *params: Any, **param2: Any) -> Any:
|
|
11
14
|
serialized = [dispatch_serializer(param) for param in params]
|
|
12
15
|
param2 = {key: dispatch_serializer(value) for key, value in param2.items()}
|
|
@@ -39,6 +42,7 @@ def input_serializer_param(position: int, name: str) -> Callable:
|
|
|
39
42
|
def output_deserializer(fonction: Callable) -> Callable:
|
|
40
43
|
"""Decorator that deserializes function output."""
|
|
41
44
|
|
|
45
|
+
@wraps(fonction)
|
|
42
46
|
def _dispatch_deserializer(self, *params: Any, **param2: Any) -> Any:
|
|
43
47
|
return dispatch_deserializer(fonction(self, *params, **param2))
|
|
44
48
|
|
|
@@ -48,6 +52,7 @@ def output_deserializer(fonction: Callable) -> Callable:
|
|
|
48
52
|
def input_deserializer(fonction: Callable) -> Callable:
|
|
49
53
|
"""Decorator that deserializes all input arguments."""
|
|
50
54
|
|
|
55
|
+
@wraps(fonction)
|
|
51
56
|
def _dispatch_deserializer(self, *params: Any, **param2: Any) -> Any:
|
|
52
57
|
serialized = [dispatch_deserializer(param) for param in params]
|
|
53
58
|
param2 = {key: dispatch_deserializer(value) for key, value in param2.items()}
|
|
@@ -59,6 +64,7 @@ def input_deserializer(fonction: Callable) -> Callable:
|
|
|
59
64
|
def output_serializer(fonction: Callable) -> Callable:
|
|
60
65
|
"""Decorator that serializes function output."""
|
|
61
66
|
|
|
67
|
+
@wraps(fonction)
|
|
62
68
|
def _dispatch_serializer(self, *params: Any, **param2: Any) -> Any:
|
|
63
69
|
return dispatch_serializer(fonction(self, *params, **param2))
|
|
64
70
|
|
iop/messages/dispatch.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from inspect import Parameter, signature
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .persistent import (
|
|
8
|
+
deserialize_persistent_message,
|
|
9
|
+
get_iris_object_classname,
|
|
10
|
+
is_persistent_message_class,
|
|
11
|
+
is_persistent_message_instance,
|
|
12
|
+
serialize_persistent_message,
|
|
13
|
+
)
|
|
14
|
+
from .serialization import (
|
|
15
|
+
deserialize_message,
|
|
16
|
+
deserialize_pickle_message,
|
|
17
|
+
serialize_message,
|
|
18
|
+
serialize_message_generator,
|
|
19
|
+
serialize_pickle_message,
|
|
20
|
+
serialize_pickle_message_generator,
|
|
21
|
+
)
|
|
22
|
+
from .validation import (
|
|
23
|
+
is_iris_object_instance,
|
|
24
|
+
is_message_class,
|
|
25
|
+
is_message_instance,
|
|
26
|
+
is_pickle_message_class,
|
|
27
|
+
is_pickle_message_instance,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_MESSAGE_CLASSES = {
|
|
31
|
+
"IOP.Message",
|
|
32
|
+
"IOP.Generator.Message.Start",
|
|
33
|
+
}
|
|
34
|
+
_PICKLE_MESSAGE_CLASSES = {
|
|
35
|
+
"IOP.PickleMessage",
|
|
36
|
+
"IOP.Generator.Message.StartPickle",
|
|
37
|
+
}
|
|
38
|
+
_HANDLER_ATTRIBUTE = "__iop_handler_message__"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class _DispatchCandidate:
|
|
43
|
+
message: str
|
|
44
|
+
method: str
|
|
45
|
+
source: str
|
|
46
|
+
priority: int
|
|
47
|
+
index: int
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def handler(message_type: Any) -> Callable[[Callable], Callable]:
|
|
51
|
+
"""Declare a method as the handler for a message type.
|
|
52
|
+
|
|
53
|
+
Use @handler(MessageType) when a BusinessProcess or BusinessOperation should
|
|
54
|
+
route a specific message type to a specific method. Typed one-argument
|
|
55
|
+
methods are also auto-discovered; on_message() remains the fallback.
|
|
56
|
+
See docs/cookbooks/add-business-process.md and
|
|
57
|
+
docs/cookbooks/add-business-operation.md.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
message_type: The message class, fully qualified class name string, or
|
|
61
|
+
IRIS object instance this method handles.
|
|
62
|
+
"""
|
|
63
|
+
message = _message_class_name(message_type)
|
|
64
|
+
if message is None:
|
|
65
|
+
raise TypeError("handler() requires a message class or class name")
|
|
66
|
+
|
|
67
|
+
def _handler(method: Callable) -> Callable:
|
|
68
|
+
setattr(method, _HANDLER_ATTRIBUTE, message)
|
|
69
|
+
return method
|
|
70
|
+
|
|
71
|
+
return _handler
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def dispatch_serializer(message: Any, is_generator: bool = False) -> Any:
|
|
75
|
+
"""Serializes the message based on its type.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
message: The message to serialize
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The serialized message
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
TypeError: If message is invalid type
|
|
85
|
+
"""
|
|
86
|
+
if message is not None:
|
|
87
|
+
if is_persistent_message_instance(message):
|
|
88
|
+
return serialize_persistent_message(message, is_generator=is_generator)
|
|
89
|
+
elif is_message_instance(message):
|
|
90
|
+
if is_generator:
|
|
91
|
+
return serialize_message_generator(message)
|
|
92
|
+
return serialize_message(message)
|
|
93
|
+
elif is_pickle_message_instance(message):
|
|
94
|
+
if is_generator:
|
|
95
|
+
return serialize_pickle_message_generator(message)
|
|
96
|
+
return serialize_pickle_message(message)
|
|
97
|
+
elif is_iris_object_instance(message):
|
|
98
|
+
return message
|
|
99
|
+
|
|
100
|
+
if message == "" or message is None:
|
|
101
|
+
return message
|
|
102
|
+
|
|
103
|
+
if hasattr(message, "__iter__"):
|
|
104
|
+
raise TypeError(
|
|
105
|
+
"You may have tried to invoke a generator function without using the 'send_generator_request' method. Please use that method to handle generator functions."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
raise TypeError(
|
|
109
|
+
"The message must be an instance of a class that is a subclass of Message or IRISObject %Persistent class."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def dispatch_deserializer(serial: Any) -> Any:
|
|
114
|
+
"""Deserializes the message based on its type.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
serial: The serialized message
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The deserialized message
|
|
121
|
+
"""
|
|
122
|
+
if serial is None or not type(serial).__module__.startswith("iris"):
|
|
123
|
+
return serial
|
|
124
|
+
|
|
125
|
+
iris_classname = get_iris_object_classname(serial)
|
|
126
|
+
|
|
127
|
+
if iris_classname in _MESSAGE_CLASSES:
|
|
128
|
+
return deserialize_message(serial)
|
|
129
|
+
|
|
130
|
+
if iris_classname in _PICKLE_MESSAGE_CLASSES:
|
|
131
|
+
return deserialize_pickle_message(serial)
|
|
132
|
+
|
|
133
|
+
deserialized = deserialize_persistent_message(serial, iris_classname=iris_classname)
|
|
134
|
+
if deserialized is not serial:
|
|
135
|
+
return deserialized
|
|
136
|
+
|
|
137
|
+
if serial._IsA("IOP.Message"):
|
|
138
|
+
return deserialize_message(serial)
|
|
139
|
+
|
|
140
|
+
if serial._IsA("IOP.PickleMessage"):
|
|
141
|
+
return deserialize_pickle_message(serial)
|
|
142
|
+
|
|
143
|
+
return serial
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def dispatch_message(host: Any, request: Any) -> Any:
|
|
147
|
+
"""Dispatches the message to the appropriate method.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
request: The request object
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
The response object
|
|
154
|
+
"""
|
|
155
|
+
call = "on_message"
|
|
156
|
+
|
|
157
|
+
module = request.__class__.__module__
|
|
158
|
+
classname = request.__class__.__name__
|
|
159
|
+
|
|
160
|
+
for msg, method in host.DISPATCH:
|
|
161
|
+
if msg == module + "." + classname:
|
|
162
|
+
return getattr(host, method)(request)
|
|
163
|
+
|
|
164
|
+
return getattr(host, call)(request)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def create_dispatch(host: Any) -> None:
|
|
168
|
+
"""Creates a dispatch table mapping class names to their handler methods.
|
|
169
|
+
The dispatch table consists of tuples of (fully_qualified_class_name, method_name).
|
|
170
|
+
Only methods that take a single typed parameter are considered as handlers.
|
|
171
|
+
"""
|
|
172
|
+
candidates: list[_DispatchCandidate] = []
|
|
173
|
+
index = 0
|
|
174
|
+
|
|
175
|
+
for message, method in _decorated_dispatch(host):
|
|
176
|
+
candidates.append(
|
|
177
|
+
_DispatchCandidate(message, method, "@handler", priority=0, index=index)
|
|
178
|
+
)
|
|
179
|
+
index += 1
|
|
180
|
+
|
|
181
|
+
for message, method in _declared_dispatch(host):
|
|
182
|
+
candidates.append(
|
|
183
|
+
_DispatchCandidate(message, method, "DISPATCH", priority=1, index=index)
|
|
184
|
+
)
|
|
185
|
+
index += 1
|
|
186
|
+
|
|
187
|
+
for method_name in get_callable_methods(host):
|
|
188
|
+
if _handler_message(getattr(host, method_name)) is not None:
|
|
189
|
+
continue
|
|
190
|
+
handler_info = get_handler_info(host, method_name)
|
|
191
|
+
if handler_info:
|
|
192
|
+
message, method = handler_info
|
|
193
|
+
candidates.append(
|
|
194
|
+
_DispatchCandidate(
|
|
195
|
+
message, method, "typed method", priority=1, index=index
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
index += 1
|
|
199
|
+
|
|
200
|
+
host.DISPATCH = _deduplicate_dispatch(host, candidates)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _declared_dispatch(host: Any) -> list[tuple[str, str]]:
|
|
204
|
+
if "DISPATCH" in getattr(host, "__dict__", {}):
|
|
205
|
+
return list(host.__dict__["DISPATCH"])
|
|
206
|
+
|
|
207
|
+
class_dispatch = host.__class__.__dict__.get("DISPATCH")
|
|
208
|
+
if class_dispatch is not None:
|
|
209
|
+
return list(class_dispatch)
|
|
210
|
+
|
|
211
|
+
return []
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _decorated_dispatch(host: Any) -> list[tuple[str, str]]:
|
|
215
|
+
dispatch = []
|
|
216
|
+
for method_name in dir(host):
|
|
217
|
+
method = getattr(host, method_name)
|
|
218
|
+
if not callable(method):
|
|
219
|
+
continue
|
|
220
|
+
message = _handler_message(method)
|
|
221
|
+
if message is not None:
|
|
222
|
+
dispatch.append((message, method_name))
|
|
223
|
+
return dispatch
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _deduplicate_dispatch(
|
|
227
|
+
host: Any, candidates: list[_DispatchCandidate]
|
|
228
|
+
) -> list[tuple[str, str]]:
|
|
229
|
+
selected: dict[str, _DispatchCandidate] = {}
|
|
230
|
+
|
|
231
|
+
for candidate in candidates:
|
|
232
|
+
current = selected.get(candidate.message)
|
|
233
|
+
if current is None:
|
|
234
|
+
selected[candidate.message] = candidate
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
if current.method == candidate.method:
|
|
238
|
+
if _is_higher_priority(candidate, current):
|
|
239
|
+
selected[candidate.message] = candidate
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
if _is_higher_priority(candidate, current):
|
|
243
|
+
_log_duplicate_mapping(host, kept=candidate, discarded=current)
|
|
244
|
+
selected[candidate.message] = candidate
|
|
245
|
+
else:
|
|
246
|
+
_log_duplicate_mapping(host, kept=current, discarded=candidate)
|
|
247
|
+
|
|
248
|
+
return [
|
|
249
|
+
(candidate.message, candidate.method)
|
|
250
|
+
for candidate in sorted(selected.values(), key=lambda item: item.index)
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _is_higher_priority(
|
|
255
|
+
candidate: _DispatchCandidate, current: _DispatchCandidate
|
|
256
|
+
) -> bool:
|
|
257
|
+
if candidate.priority != current.priority:
|
|
258
|
+
return candidate.priority < current.priority
|
|
259
|
+
return candidate.index > current.index
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _log_duplicate_mapping(
|
|
263
|
+
host: Any, kept: _DispatchCandidate, discarded: _DispatchCandidate
|
|
264
|
+
) -> None:
|
|
265
|
+
message = (
|
|
266
|
+
f"Duplicate dispatch mapping for {kept.message}: "
|
|
267
|
+
f"keeping {kept.method} from {kept.source}; "
|
|
268
|
+
f"discarding {discarded.method} from {discarded.source}."
|
|
269
|
+
)
|
|
270
|
+
log_warning = getattr(host, "log_warning", None)
|
|
271
|
+
if callable(log_warning):
|
|
272
|
+
try:
|
|
273
|
+
log_warning(message)
|
|
274
|
+
return
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
logging.getLogger(__name__).warning(message)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _handler_message(method: Any) -> str | None:
|
|
282
|
+
message = getattr(method, _HANDLER_ATTRIBUTE, None)
|
|
283
|
+
if message is not None:
|
|
284
|
+
return message
|
|
285
|
+
|
|
286
|
+
func = getattr(method, "__func__", None)
|
|
287
|
+
return getattr(func, _HANDLER_ATTRIBUTE, None)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def get_callable_methods(host: Any) -> list[str]:
|
|
291
|
+
"""Returns a list of callable method names that don't start with underscore."""
|
|
292
|
+
return [
|
|
293
|
+
func
|
|
294
|
+
for func in dir(host)
|
|
295
|
+
if callable(getattr(host, func)) and not func.startswith("_")
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def get_handler_info(host: Any, method_name: str) -> tuple[str, str] | None:
|
|
300
|
+
"""Analyzes a method to determine if it's a valid message handler.
|
|
301
|
+
Returns a tuple of (fully_qualified_class_name, method_name) if valid,
|
|
302
|
+
None otherwise.
|
|
303
|
+
"""
|
|
304
|
+
try:
|
|
305
|
+
params = signature(getattr(host, method_name)).parameters
|
|
306
|
+
if len(params) != 1:
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
param: Parameter = next(iter(params.values()))
|
|
310
|
+
annotation = param.annotation
|
|
311
|
+
|
|
312
|
+
if annotation == Parameter.empty:
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
message = _message_class_name(annotation)
|
|
316
|
+
if message is None:
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
return message, method_name
|
|
320
|
+
|
|
321
|
+
except ValueError:
|
|
322
|
+
return None
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _message_class_name(message_type: Any) -> str | None:
|
|
326
|
+
if isinstance(message_type, str):
|
|
327
|
+
return message_type
|
|
328
|
+
|
|
329
|
+
if is_iris_object_instance(message_type):
|
|
330
|
+
return f"{type(message_type).__module__}.{type(message_type).__name__}"
|
|
331
|
+
|
|
332
|
+
if message_type is Any or message_type is object:
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
if not isinstance(message_type, type):
|
|
336
|
+
return None
|
|
337
|
+
|
|
338
|
+
if not _is_dispatch_message_class(message_type):
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
return f"{message_type.__module__}.{message_type.__name__}"
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _is_dispatch_message_class(klass: type) -> bool:
|
|
345
|
+
if is_message_class(klass):
|
|
346
|
+
return True
|
|
347
|
+
if is_pickle_message_class(klass):
|
|
348
|
+
return True
|
|
349
|
+
if is_persistent_message_class(klass):
|
|
350
|
+
return True
|
|
351
|
+
return klass.__module__.startswith("iris")
|