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.
Files changed (119) hide show
  1. iop/__init__.py +276 -18
  2. iop/__main__.py +3 -3
  3. iop/cli/__init__.py +1 -0
  4. iop/cli/formatting.py +45 -0
  5. iop/{_cli.py → cli/main.py} +238 -216
  6. iop/cli/parser.py +165 -0
  7. iop/cli/types.py +73 -0
  8. iop/cls/IOP/Common.cls +11 -2
  9. iop/cls/IOP/PrivateSession/Duplex.cls +1 -1
  10. iop/cls/IOP/Service/Remote/Rest/v1.cls +152 -5
  11. iop/cls/IOP/Utils.cls +814 -7
  12. iop/cls/IOP/Wrapper.cls +1 -1
  13. iop/components/__init__.py +16 -0
  14. iop/{_async_request.py → components/async_request.py} +7 -7
  15. iop/components/business_host.py +363 -0
  16. iop/components/business_operation.py +84 -0
  17. iop/{_business_process.py → components/business_process.py} +145 -125
  18. iop/components/business_service.py +106 -0
  19. iop/{_common.py → components/common.py} +160 -164
  20. iop/{_debugpy.py → components/debugpy.py} +11 -11
  21. iop/{_generator_request.py → components/generator_request.py} +4 -4
  22. iop/{_inbound_adapter.py → components/inbound_adapter.py} +10 -18
  23. iop/{_log_manager.py → components/log_manager.py} +1 -1
  24. iop/{_outbound_adapter.py → components/outbound_adapter.py} +1 -1
  25. iop/components/polling_business_service.py +48 -0
  26. iop/{_private_session_duplex.py → components/private_session_duplex.py} +7 -9
  27. iop/{_private_session_process.py → components/private_session_process.py} +2 -2
  28. iop/{_settings.py → components/settings.py} +23 -3
  29. iop/messages/__init__.py +7 -0
  30. iop/messages/base.py +61 -0
  31. iop/{_decorators.py → messages/decorators.py} +8 -2
  32. iop/messages/dispatch.py +351 -0
  33. iop/{_persistent_message.py → messages/persistent.py} +24 -19
  34. iop/{_serialization.py → messages/serialization.py} +34 -21
  35. iop/{_message_validator.py → messages/validation.py} +15 -13
  36. iop/migration/__init__.py +1 -0
  37. iop/migration/io.py +61 -0
  38. iop/migration/manifest.py +534 -0
  39. iop/migration/plans.py +80 -0
  40. iop/migration/utils.py +1126 -0
  41. iop/production/__init__.py +55 -0
  42. iop/production/actions.py +525 -0
  43. iop/production/common.py +103 -0
  44. iop/production/component.py +234 -0
  45. iop/production/declarations.py +190 -0
  46. iop/production/declarative.py +214 -0
  47. iop/production/diff.py +327 -0
  48. iop/production/import_.py +296 -0
  49. iop/production/inspection.py +92 -0
  50. iop/production/model.py +1143 -0
  51. iop/production/planning.py +678 -0
  52. iop/production/reconstruction.py +341 -0
  53. iop/production/rendering.py +610 -0
  54. iop/production/runtime.py +91 -0
  55. iop/production/source_inference.py +729 -0
  56. iop/production/types.py +546 -0
  57. iop/production/validation.py +452 -0
  58. iop/runtime/__init__.py +1 -0
  59. iop/runtime/director.py +415 -0
  60. iop/runtime/environment.py +59 -0
  61. iop/{_iris.py → runtime/iris.py} +2 -2
  62. iop/runtime/local.py +145 -0
  63. iop/{_director_protocol.py → runtime/protocol.py} +25 -8
  64. iop/runtime/remote/__init__.py +12 -0
  65. iop/runtime/remote/client.py +90 -0
  66. iop/runtime/remote/director.py +283 -0
  67. iop/runtime/remote/migration.py +57 -0
  68. iop/runtime/remote/settings.py +69 -0
  69. iop/runtime/remote/setup.py +72 -0
  70. {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/METADATA +11 -2
  71. iris_pex_embedded_python-4.0.0.dist-info/RECORD +99 -0
  72. iris_pex_embedded_python-4.0.0.dist-info/entry_points.txt +2 -0
  73. iris_pex_embedded_python-4.0.0.dist-info/top_level.txt +1 -0
  74. grongier/__init__.py +0 -0
  75. grongier/cls/Grongier/PEX/BusinessOperation.cls +0 -8
  76. grongier/cls/Grongier/PEX/BusinessProcess.cls +0 -13
  77. grongier/cls/Grongier/PEX/BusinessService.cls +0 -8
  78. grongier/cls/Grongier/PEX/Common.cls +0 -10
  79. grongier/cls/Grongier/PEX/Director.cls +0 -10
  80. grongier/cls/Grongier/PEX/Duplex/Operation.cls +0 -4
  81. grongier/cls/Grongier/PEX/Duplex/Process.cls +0 -13
  82. grongier/cls/Grongier/PEX/Duplex/Service.cls +0 -4
  83. grongier/cls/Grongier/PEX/InboundAdapter.cls +0 -8
  84. grongier/cls/Grongier/PEX/Message.cls +0 -13
  85. grongier/cls/Grongier/PEX/OutboundAdapter.cls +0 -8
  86. grongier/cls/Grongier/PEX/PickleMessage.cls +0 -13
  87. grongier/cls/Grongier/PEX/PrivateSession/Duplex.cls +0 -8
  88. grongier/cls/Grongier/PEX/PrivateSession/Message/Ack.cls +0 -14
  89. grongier/cls/Grongier/PEX/PrivateSession/Message/Poll.cls +0 -14
  90. grongier/cls/Grongier/PEX/PrivateSession/Message/Start.cls +0 -14
  91. grongier/cls/Grongier/PEX/PrivateSession/Message/Stop.cls +0 -14
  92. grongier/cls/Grongier/PEX/Test.cls +0 -10
  93. grongier/cls/Grongier/PEX/Utils.cls +0 -10
  94. grongier/cls/Grongier/Service/WSGI.cls +0 -4
  95. grongier/pex/__init__.py +0 -75
  96. grongier/pex/__main__.py +0 -4
  97. grongier/pex/_business_host.py +0 -1
  98. grongier/pex/_cli.py +0 -4
  99. grongier/pex/_common.py +0 -1
  100. grongier/pex/_director.py +0 -1
  101. grongier/pex/_utils.py +0 -1
  102. grongier/pex/wsgi/handlers.py +0 -104
  103. iop/_business_host.py +0 -322
  104. iop/_business_operation.py +0 -77
  105. iop/_business_service.py +0 -67
  106. iop/_director.py +0 -333
  107. iop/_dispatch.py +0 -194
  108. iop/_local.py +0 -106
  109. iop/_message.py +0 -45
  110. iop/_polling_business_service.py +0 -6
  111. iop/_remote.py +0 -473
  112. iop/_utils.py +0 -912
  113. iop/cls/IOP/Service/WSGI.cls +0 -310
  114. iop/wsgi/handlers.py +0 -112
  115. iris_pex_embedded_python-3.7.2b2.dist-info/RECORD +0 -96
  116. iris_pex_embedded_python-3.7.2b2.dist-info/entry_points.txt +0 -2
  117. iris_pex_embedded_python-3.7.2b2.dist-info/top_level.txt +0 -2
  118. {iris_pex_embedded_python-3.7.2b2.dist-info → iris_pex_embedded_python-4.0.0.dist-info}/WHEEL +0 -0
  119. {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 ._business_host import _BusinessHost
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 ._dispatch import dispach_message
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 the SendRequestAsync() or the SendRequestSync() method
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 dispach_message(self, request)
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("Grongier.PEX.InboundAdapter")
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 ._business_process import _BusinessProcess
2
- from ._decorators import input_deserializer, output_serializer
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 enum import Enum
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
 
@@ -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, Callable
3
+ from typing import Any
3
4
 
4
- from ._dispatch import dispatch_deserializer, dispatch_serializer
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
 
@@ -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")