ezmsg-baseproc 1.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.
- ezmsg/baseproc/__init__.py +155 -0
- ezmsg/baseproc/__version__.py +34 -0
- ezmsg/baseproc/composite.py +323 -0
- ezmsg/baseproc/processor.py +209 -0
- ezmsg/baseproc/protocols.py +147 -0
- ezmsg/baseproc/stateful.py +323 -0
- ezmsg/baseproc/units.py +282 -0
- ezmsg/baseproc/util/__init__.py +1 -0
- ezmsg/baseproc/util/asio.py +138 -0
- ezmsg/baseproc/util/message.py +31 -0
- ezmsg/baseproc/util/profile.py +171 -0
- ezmsg/baseproc/util/typeresolution.py +81 -0
- ezmsg_baseproc-1.0.dist-info/METADATA +106 -0
- ezmsg_baseproc-1.0.dist-info/RECORD +16 -0
- ezmsg_baseproc-1.0.dist-info/WHEEL +4 -0
- ezmsg_baseproc-1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ezmsg-baseproc: Base processor classes for ezmsg.
|
|
3
|
+
|
|
4
|
+
This package provides the foundational processor architecture for building
|
|
5
|
+
signal processing pipelines in ezmsg.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .__version__ import __version__ as __version__
|
|
9
|
+
|
|
10
|
+
# Composite processor classes
|
|
11
|
+
from .composite import (
|
|
12
|
+
CompositeProcessor,
|
|
13
|
+
CompositeProducer,
|
|
14
|
+
CompositeStateful,
|
|
15
|
+
_get_processor_message_type,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Base processor classes (non-stateful)
|
|
19
|
+
from .processor import (
|
|
20
|
+
BaseConsumer,
|
|
21
|
+
BaseProcessor,
|
|
22
|
+
BaseProducer,
|
|
23
|
+
BaseTransformer,
|
|
24
|
+
_get_base_processor_message_in_type,
|
|
25
|
+
_get_base_processor_message_out_type,
|
|
26
|
+
_get_base_processor_settings_type,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Protocols and type variables
|
|
30
|
+
from .protocols import (
|
|
31
|
+
AdaptiveTransformer,
|
|
32
|
+
Consumer,
|
|
33
|
+
MessageInType,
|
|
34
|
+
MessageOutType,
|
|
35
|
+
Processor,
|
|
36
|
+
Producer,
|
|
37
|
+
SettingsType,
|
|
38
|
+
StatefulConsumer,
|
|
39
|
+
StatefulProcessor,
|
|
40
|
+
StatefulProducer,
|
|
41
|
+
StatefulTransformer,
|
|
42
|
+
StateType,
|
|
43
|
+
Transformer,
|
|
44
|
+
processor_state,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Stateful processor classes
|
|
48
|
+
from .stateful import (
|
|
49
|
+
BaseAdaptiveTransformer,
|
|
50
|
+
BaseAsyncTransformer,
|
|
51
|
+
BaseStatefulConsumer,
|
|
52
|
+
BaseStatefulProcessor,
|
|
53
|
+
BaseStatefulProducer,
|
|
54
|
+
BaseStatefulTransformer,
|
|
55
|
+
Stateful,
|
|
56
|
+
_get_base_processor_state_type,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Unit classes for ezmsg integration
|
|
60
|
+
from .units import (
|
|
61
|
+
AdaptiveTransformerType,
|
|
62
|
+
BaseAdaptiveTransformerUnit,
|
|
63
|
+
BaseConsumerUnit,
|
|
64
|
+
BaseProcessorUnit,
|
|
65
|
+
BaseProducerUnit,
|
|
66
|
+
BaseTransformerUnit,
|
|
67
|
+
ConsumerType,
|
|
68
|
+
GenAxisArray,
|
|
69
|
+
ProducerType,
|
|
70
|
+
TransformerType,
|
|
71
|
+
get_base_adaptive_transformer_type,
|
|
72
|
+
get_base_consumer_type,
|
|
73
|
+
get_base_producer_type,
|
|
74
|
+
get_base_transformer_type,
|
|
75
|
+
)
|
|
76
|
+
from .util.asio import CoroutineExecutionError, SyncToAsyncGeneratorWrapper, run_coroutine_sync
|
|
77
|
+
|
|
78
|
+
# Utility classes and functions
|
|
79
|
+
from .util.message import SampleMessage, SampleTriggerMessage, is_sample_message
|
|
80
|
+
from .util.profile import profile_method, profile_subpub
|
|
81
|
+
from .util.typeresolution import check_message_type_compatibility, resolve_typevar
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
# Version
|
|
85
|
+
"__version__",
|
|
86
|
+
# Protocols
|
|
87
|
+
"Processor",
|
|
88
|
+
"Producer",
|
|
89
|
+
"Consumer",
|
|
90
|
+
"Transformer",
|
|
91
|
+
"StatefulProcessor",
|
|
92
|
+
"StatefulProducer",
|
|
93
|
+
"StatefulConsumer",
|
|
94
|
+
"StatefulTransformer",
|
|
95
|
+
"AdaptiveTransformer",
|
|
96
|
+
# Type variables
|
|
97
|
+
"MessageInType",
|
|
98
|
+
"MessageOutType",
|
|
99
|
+
"SettingsType",
|
|
100
|
+
"StateType",
|
|
101
|
+
"ProducerType",
|
|
102
|
+
"ConsumerType",
|
|
103
|
+
"TransformerType",
|
|
104
|
+
"AdaptiveTransformerType",
|
|
105
|
+
# Decorators
|
|
106
|
+
"processor_state",
|
|
107
|
+
# Base processor classes
|
|
108
|
+
"BaseProcessor",
|
|
109
|
+
"BaseProducer",
|
|
110
|
+
"BaseConsumer",
|
|
111
|
+
"BaseTransformer",
|
|
112
|
+
# Stateful classes
|
|
113
|
+
"Stateful",
|
|
114
|
+
"BaseStatefulProcessor",
|
|
115
|
+
"BaseStatefulProducer",
|
|
116
|
+
"BaseStatefulConsumer",
|
|
117
|
+
"BaseStatefulTransformer",
|
|
118
|
+
"BaseAdaptiveTransformer",
|
|
119
|
+
"BaseAsyncTransformer",
|
|
120
|
+
# Composite classes
|
|
121
|
+
"CompositeStateful",
|
|
122
|
+
"CompositeProcessor",
|
|
123
|
+
"CompositeProducer",
|
|
124
|
+
# Unit classes
|
|
125
|
+
"BaseProducerUnit",
|
|
126
|
+
"BaseProcessorUnit",
|
|
127
|
+
"BaseConsumerUnit",
|
|
128
|
+
"BaseTransformerUnit",
|
|
129
|
+
"BaseAdaptiveTransformerUnit",
|
|
130
|
+
"GenAxisArray",
|
|
131
|
+
# Type resolution helpers
|
|
132
|
+
"get_base_producer_type",
|
|
133
|
+
"get_base_consumer_type",
|
|
134
|
+
"get_base_transformer_type",
|
|
135
|
+
"get_base_adaptive_transformer_type",
|
|
136
|
+
"_get_base_processor_settings_type",
|
|
137
|
+
"_get_base_processor_message_in_type",
|
|
138
|
+
"_get_base_processor_message_out_type",
|
|
139
|
+
"_get_base_processor_state_type",
|
|
140
|
+
"_get_processor_message_type",
|
|
141
|
+
# Message types
|
|
142
|
+
"SampleMessage",
|
|
143
|
+
"SampleTriggerMessage",
|
|
144
|
+
"is_sample_message",
|
|
145
|
+
# Profiling
|
|
146
|
+
"profile_method",
|
|
147
|
+
"profile_subpub",
|
|
148
|
+
# Async utilities
|
|
149
|
+
"CoroutineExecutionError",
|
|
150
|
+
"SyncToAsyncGeneratorWrapper",
|
|
151
|
+
"run_coroutine_sync",
|
|
152
|
+
# Type utilities
|
|
153
|
+
"check_message_type_compatibility",
|
|
154
|
+
"resolve_typevar",
|
|
155
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '1.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Composite processor classes for building pipelines."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import pickle
|
|
5
|
+
import typing
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from types import GeneratorType
|
|
8
|
+
|
|
9
|
+
from .processor import BaseProcessor, BaseProducer
|
|
10
|
+
from .protocols import MessageInType, MessageOutType, SettingsType
|
|
11
|
+
from .stateful import Stateful
|
|
12
|
+
from .util.asio import SyncToAsyncGeneratorWrapper
|
|
13
|
+
from .util.typeresolution import check_message_type_compatibility
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_processor_message_type(
|
|
17
|
+
proc: BaseProcessor | BaseProducer | GeneratorType | SyncToAsyncGeneratorWrapper,
|
|
18
|
+
dir: str,
|
|
19
|
+
) -> type | None:
|
|
20
|
+
"""Extract the input type from a processor."""
|
|
21
|
+
if isinstance(proc, GeneratorType) or isinstance(proc, SyncToAsyncGeneratorWrapper):
|
|
22
|
+
gen_func = proc.gi_frame.f_globals[proc.gi_frame.f_code.co_name]
|
|
23
|
+
args = typing.get_args(gen_func.__annotations__.get("return"))
|
|
24
|
+
return args[0] if dir == "out" else args[1] # yield type / send type
|
|
25
|
+
return proc.__class__.get_message_type(dir)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _has_stateful_op(proc: typing.Any) -> typing.TypeGuard[Stateful]:
|
|
29
|
+
"""
|
|
30
|
+
Check if the processor has a stateful_op method.
|
|
31
|
+
This is used to determine if the processor is stateful or not.
|
|
32
|
+
"""
|
|
33
|
+
return hasattr(proc, "stateful_op")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CompositeStateful(Stateful[dict[str, typing.Any]], ABC, typing.Generic[SettingsType, MessageOutType]):
|
|
37
|
+
"""
|
|
38
|
+
Mixin class for composite processor/producer chains. DO NOT use this class directly.
|
|
39
|
+
Used to enforce statefulness of the composite processor/producer chain and provide
|
|
40
|
+
initialization and validation methods.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
_procs: dict[str, BaseProducer | BaseProcessor | GeneratorType | SyncToAsyncGeneratorWrapper]
|
|
44
|
+
_processor_type: typing.Literal["producer", "processor"]
|
|
45
|
+
|
|
46
|
+
def _validate_processor_chain(self) -> None:
|
|
47
|
+
"""Validate the composite chain types at runtime."""
|
|
48
|
+
if not self._procs:
|
|
49
|
+
raise ValueError(f"Composite {self._processor_type} requires at least one processor")
|
|
50
|
+
|
|
51
|
+
expected_in_type = _get_processor_message_type(self, "in")
|
|
52
|
+
expected_out_type = _get_processor_message_type(self, "out")
|
|
53
|
+
|
|
54
|
+
procs = [p for p in self._procs.items() if p[1] is not None]
|
|
55
|
+
in_type = _get_processor_message_type(procs[0][1], "in")
|
|
56
|
+
if not check_message_type_compatibility(expected_in_type, in_type):
|
|
57
|
+
raise TypeError(
|
|
58
|
+
f"Input type mismatch: Composite {self._processor_type} expects {expected_in_type}, "
|
|
59
|
+
f"but its first processor (name: {procs[0][0]}, type: {procs[0][1].__class__.__name__}) "
|
|
60
|
+
f"accepts {in_type}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
out_type = _get_processor_message_type(procs[-1][1], "out")
|
|
64
|
+
if not check_message_type_compatibility(out_type, expected_out_type):
|
|
65
|
+
raise TypeError(
|
|
66
|
+
f"Output type mismatch: Composite {self._processor_type} wants to return {expected_out_type}, "
|
|
67
|
+
f"but its last processor (name: {procs[-1][0]}, type: {procs[-1][1].__class__.__name__}) "
|
|
68
|
+
f"returns {out_type}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Check intermediate connections
|
|
72
|
+
for i in range(len(procs) - 1):
|
|
73
|
+
current_out_type = _get_processor_message_type(procs[i][1], "out")
|
|
74
|
+
next_in_type = _get_processor_message_type(procs[i + 1][1], "in")
|
|
75
|
+
|
|
76
|
+
if current_out_type is None or current_out_type is type(None):
|
|
77
|
+
raise TypeError(
|
|
78
|
+
f"Processor {i} (name: {procs[i][0]}, type: {procs[i][1].__class__.__name__}) is a consumer "
|
|
79
|
+
"or returns None. Consumers can only be the last processor of a "
|
|
80
|
+
f"composite {self._processor_type} chain."
|
|
81
|
+
)
|
|
82
|
+
if next_in_type is None or next_in_type is type(None):
|
|
83
|
+
raise TypeError(
|
|
84
|
+
f"Processor {i + 1} (name: {procs[i + 1][0]}, type: {procs[i + 1][1].__class__.__name__}) "
|
|
85
|
+
f"is a producer or receives only None. Producers can only be the first processor of a composite "
|
|
86
|
+
f"producer chain."
|
|
87
|
+
)
|
|
88
|
+
if not check_message_type_compatibility(current_out_type, next_in_type):
|
|
89
|
+
raise TypeError(
|
|
90
|
+
f"Message type mismatch between processors {i} (name: {procs[i][0]}, "
|
|
91
|
+
f"type: {procs[i][1].__class__.__name__}) "
|
|
92
|
+
f"and {i + 1} (name: {procs[i + 1][0]}, type: {procs[i + 1][1].__class__.__name__}): "
|
|
93
|
+
f"{procs[i][1].__class__.__name__} outputs {current_out_type}, "
|
|
94
|
+
f"but {procs[i + 1][1].__class__.__name__} expects {next_in_type}"
|
|
95
|
+
)
|
|
96
|
+
if inspect.isgenerator(procs[i][1]) and hasattr(procs[i][1], "send"):
|
|
97
|
+
# If the processor is a generator, wrap it in a SyncToAsyncGeneratorWrapper
|
|
98
|
+
procs[i] = (procs[i][0], SyncToAsyncGeneratorWrapper(procs[i][1]))
|
|
99
|
+
if inspect.isgenerator(procs[-1][1]) and hasattr(procs[-1][1], "send"):
|
|
100
|
+
# If the last processor is a generator, wrap it in a SyncToAsyncGeneratorWrapper
|
|
101
|
+
procs[-1] = (procs[-1][0], SyncToAsyncGeneratorWrapper(procs[-1][1]))
|
|
102
|
+
self._procs = {k: v for (k, v) in procs}
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def _initialize_processors(
|
|
107
|
+
settings: SettingsType,
|
|
108
|
+
) -> dict[str, typing.Any]: ...
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def state(self) -> dict[str, typing.Any]:
|
|
112
|
+
return {k: getattr(proc, "state") for k, proc in self._procs.items() if hasattr(proc, "state")}
|
|
113
|
+
|
|
114
|
+
@state.setter
|
|
115
|
+
def state(self, state: dict[str, typing.Any] | bytes | None) -> None:
|
|
116
|
+
if state is not None:
|
|
117
|
+
if isinstance(state, bytes):
|
|
118
|
+
state = pickle.loads(state)
|
|
119
|
+
for k, v in state.items(): # type: ignore
|
|
120
|
+
if k not in self._procs:
|
|
121
|
+
raise KeyError(
|
|
122
|
+
f"Processor (name: {k}) in provided state not found in composite {self._processor_type} chain. "
|
|
123
|
+
f"Available keys: {list(self._procs.keys())}"
|
|
124
|
+
)
|
|
125
|
+
if hasattr(self._procs[k], "state"):
|
|
126
|
+
setattr(self._procs[k], "state", v)
|
|
127
|
+
|
|
128
|
+
def _reset_state(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
|
129
|
+
# By default, we don't expect to change the state of a composite processor/producer
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
@abstractmethod
|
|
133
|
+
def stateful_op(
|
|
134
|
+
self,
|
|
135
|
+
state: dict[str, tuple[typing.Any, int]] | None,
|
|
136
|
+
*args: typing.Any,
|
|
137
|
+
**kwargs: typing.Any,
|
|
138
|
+
) -> tuple[
|
|
139
|
+
dict[str, tuple[typing.Any, int]],
|
|
140
|
+
MessageOutType | None,
|
|
141
|
+
]: ...
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class CompositeProcessor(
|
|
145
|
+
BaseProcessor[SettingsType, MessageInType, MessageOutType],
|
|
146
|
+
CompositeStateful[SettingsType, MessageOutType],
|
|
147
|
+
ABC,
|
|
148
|
+
typing.Generic[SettingsType, MessageInType, MessageOutType],
|
|
149
|
+
):
|
|
150
|
+
"""
|
|
151
|
+
A processor that chains multiple processor together in a feedforward non-branching graph.
|
|
152
|
+
The individual processors may be stateless or stateful. The last processor may be a consumer,
|
|
153
|
+
otherwise processors must be transformers. Use CompositeProducer if you want the first
|
|
154
|
+
processor to be a producer. Concrete subclasses must implement `_initialize_processors`.
|
|
155
|
+
Optionally override `_reset_state` if you want adaptive state behaviour.
|
|
156
|
+
Example implementation:
|
|
157
|
+
|
|
158
|
+
class CustomCompositeProcessor(CompositeProcessor[CustomSettings, AxisArray, AxisArray]):
|
|
159
|
+
@staticmethod
|
|
160
|
+
def _initialize_processors(settings: CustomSettings) -> dict[str, BaseProcessor]:
|
|
161
|
+
return {
|
|
162
|
+
"stateful_transformer": CustomStatefulProducer(**settings),
|
|
163
|
+
"transformer": CustomTransformer(**settings),
|
|
164
|
+
}
|
|
165
|
+
Where **settings should be replaced with initialisation arguments for each processor.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
169
|
+
super().__init__(*args, **kwargs) # .settings
|
|
170
|
+
self._processor_type = "processor"
|
|
171
|
+
self._procs = self._initialize_processors(self.settings)
|
|
172
|
+
self._validate_processor_chain()
|
|
173
|
+
first_proc = next(iter(self._procs.items()))
|
|
174
|
+
first_proc_in_type = _get_processor_message_type(first_proc[1], "in")
|
|
175
|
+
if first_proc_in_type is None or first_proc_in_type is type(None):
|
|
176
|
+
raise TypeError(
|
|
177
|
+
f"First processor (name: {first_proc[0]}, type: {first_proc[1].__class__.__name__}) "
|
|
178
|
+
f"is a producer or receives only None. Please use CompositeProducer, not "
|
|
179
|
+
f"CompositeProcessor for this composite chain."
|
|
180
|
+
)
|
|
181
|
+
self._hash = -1
|
|
182
|
+
|
|
183
|
+
@staticmethod
|
|
184
|
+
@abstractmethod
|
|
185
|
+
def _initialize_processors(settings: SettingsType) -> dict[str, typing.Any]: ...
|
|
186
|
+
|
|
187
|
+
def _process(self, message: MessageInType | None = None) -> MessageOutType | None:
|
|
188
|
+
"""
|
|
189
|
+
Process a message through the pipeline of processors. If the message is None, or no message is provided,
|
|
190
|
+
then it will be assumed that the first processor is a producer and will be called without arguments.
|
|
191
|
+
This will be invoked via `__call__` or `send`.
|
|
192
|
+
We use `__next__` and `send` to allow using legacy generators that have yet to be converted to transformers.
|
|
193
|
+
|
|
194
|
+
Warning: All processors will be called using their synchronous API, which may invoke a slow sync->async wrapper
|
|
195
|
+
for processors that are async-first (i.e., children of BaseProducer or BaseAsyncTransformer).
|
|
196
|
+
If you are in an async context, please use instead this object's `asend` or `__acall__`,
|
|
197
|
+
which is much faster for async processors and does not incur penalty on sync processors.
|
|
198
|
+
"""
|
|
199
|
+
result = message
|
|
200
|
+
for proc in self._procs.values():
|
|
201
|
+
result = proc.send(result)
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
async def _aprocess(self, message: MessageInType | None = None) -> MessageOutType | None:
|
|
205
|
+
"""
|
|
206
|
+
Process a message through the pipeline of processors using their async APIs.
|
|
207
|
+
If the message is None, or no message is provided, then it will be assumed that the first processor
|
|
208
|
+
is a producer and will be called without arguments.
|
|
209
|
+
We use `__anext__` and `asend` to allow using legacy generators that have yet to be converted to transformers.
|
|
210
|
+
"""
|
|
211
|
+
result = message
|
|
212
|
+
for proc in self._procs.values():
|
|
213
|
+
result = await proc.asend(result)
|
|
214
|
+
return result
|
|
215
|
+
|
|
216
|
+
def stateful_op(
|
|
217
|
+
self,
|
|
218
|
+
state: dict[str, tuple[typing.Any, int]] | None,
|
|
219
|
+
message: MessageInType | None,
|
|
220
|
+
) -> tuple[
|
|
221
|
+
dict[str, tuple[typing.Any, int]],
|
|
222
|
+
MessageOutType | None,
|
|
223
|
+
]:
|
|
224
|
+
result = message
|
|
225
|
+
state = state or {}
|
|
226
|
+
try:
|
|
227
|
+
state_keys = list(state.keys())
|
|
228
|
+
except AttributeError as e:
|
|
229
|
+
raise AttributeError("state provided to stateful_op must be a dict or None") from e
|
|
230
|
+
for key in state_keys:
|
|
231
|
+
if key not in self._procs:
|
|
232
|
+
raise KeyError(
|
|
233
|
+
f"Processor (name: {key}) in provided state not found in composite processor chain. "
|
|
234
|
+
f"Available keys: {list(self._procs.keys())}"
|
|
235
|
+
)
|
|
236
|
+
for k, proc in self._procs.items():
|
|
237
|
+
if _has_stateful_op(proc):
|
|
238
|
+
state[k], result = proc.stateful_op(state.get(k, None), result)
|
|
239
|
+
else:
|
|
240
|
+
result = proc.send(result)
|
|
241
|
+
return state, result
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class CompositeProducer(
|
|
245
|
+
BaseProducer[SettingsType, MessageOutType],
|
|
246
|
+
CompositeStateful[SettingsType, MessageOutType],
|
|
247
|
+
ABC,
|
|
248
|
+
typing.Generic[SettingsType, MessageOutType],
|
|
249
|
+
):
|
|
250
|
+
"""
|
|
251
|
+
A producer that chains multiple processors (starting with a producer) together in a feedforward
|
|
252
|
+
non-branching graph. The individual processors may be stateless or stateful.
|
|
253
|
+
The first processor must be a producer, the last processor may be a consumer, otherwise
|
|
254
|
+
processors must be transformers.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
258
|
+
super().__init__(*args, **kwargs) # .settings
|
|
259
|
+
self._processor_type = "producer"
|
|
260
|
+
self._procs = self._initialize_processors(self.settings)
|
|
261
|
+
self._validate_processor_chain()
|
|
262
|
+
first_proc = next(iter(self._procs.items()))
|
|
263
|
+
first_proc_in_type = _get_processor_message_type(first_proc[1], "in")
|
|
264
|
+
if first_proc_in_type is not None and first_proc_in_type is not type(None):
|
|
265
|
+
raise TypeError(
|
|
266
|
+
f"First processor (name: {first_proc[0]}, type: {first_proc[1].__class__.__name__}) "
|
|
267
|
+
f"is not a producer. Please use CompositeProcessor, not "
|
|
268
|
+
f"CompositeProducer for this composite chain."
|
|
269
|
+
)
|
|
270
|
+
self._hash = -1
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
@abstractmethod
|
|
274
|
+
def _initialize_processors(
|
|
275
|
+
settings: SettingsType,
|
|
276
|
+
) -> dict[str, typing.Any]: ...
|
|
277
|
+
|
|
278
|
+
async def _produce(self) -> MessageOutType:
|
|
279
|
+
"""
|
|
280
|
+
Process a message through the pipeline of processors. If the message is None, or no message is provided,
|
|
281
|
+
then it will be assumed that the first processor is a producer and will be called without arguments.
|
|
282
|
+
This will be invoked via `__call__` or `send`.
|
|
283
|
+
We use `__next__` and `send` to allow using legacy generators that have yet to be converted to transformers.
|
|
284
|
+
|
|
285
|
+
Warning: All processors will be called using their asynchronous API, which is much faster for async
|
|
286
|
+
processors and does not incur penalty on sync processors.
|
|
287
|
+
"""
|
|
288
|
+
procs = list(self._procs.values())
|
|
289
|
+
result = await procs[0].__anext__()
|
|
290
|
+
for proc in procs[1:]:
|
|
291
|
+
result = await proc.asend(result)
|
|
292
|
+
return result
|
|
293
|
+
|
|
294
|
+
def stateful_op(
|
|
295
|
+
self,
|
|
296
|
+
state: dict[str, tuple[typing.Any, int]] | None,
|
|
297
|
+
) -> tuple[
|
|
298
|
+
dict[str, tuple[typing.Any, int]],
|
|
299
|
+
MessageOutType | None,
|
|
300
|
+
]:
|
|
301
|
+
state = state or {}
|
|
302
|
+
try:
|
|
303
|
+
state_keys = list(state.keys())
|
|
304
|
+
except AttributeError as e:
|
|
305
|
+
raise AttributeError("state provided to stateful_op must be a dict or None") from e
|
|
306
|
+
for key in state_keys:
|
|
307
|
+
if key not in self._procs:
|
|
308
|
+
raise KeyError(
|
|
309
|
+
f"Processor (name: {key}) in provided state not found in composite producer chain. "
|
|
310
|
+
f"Available keys: {list(self._procs.keys())}"
|
|
311
|
+
)
|
|
312
|
+
labeled_procs = list(self._procs.items())
|
|
313
|
+
prod_name, prod = labeled_procs[0]
|
|
314
|
+
if _has_stateful_op(prod):
|
|
315
|
+
state[prod_name], result = prod.stateful_op(state.get(prod_name, None))
|
|
316
|
+
else:
|
|
317
|
+
result = prod.__next__()
|
|
318
|
+
for k, proc in labeled_procs[1:]:
|
|
319
|
+
if _has_stateful_op(proc):
|
|
320
|
+
state[k], result = proc.stateful_op(state.get(k, None), result)
|
|
321
|
+
else:
|
|
322
|
+
result = proc.send(result)
|
|
323
|
+
return state, result
|