openrois-components-core 0.1.0a1__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.
@@ -0,0 +1,41 @@
1
+ """OpenRoIS component SDK: write a RoIS HRI Component as a Python class.
2
+
3
+ A component subclasses Component, declares its type with ``@component(profile)``, and
4
+ marks the messages it implements::
5
+
6
+ from openrois.components.core import Component, component, invoke, subscribe
7
+ from openrois.interfaces.components import NAVIGATION_PROFILE
8
+
9
+ @component(NAVIGATION_PROFILE)
10
+ class Navigation(Component):
11
+ @invoke("start")
12
+ async def start(self) -> None:
13
+ ...
14
+
15
+ The engine that hosts the component stores its parameters, answers component_status,
16
+ tracks its commands and serves the part of the profile it implements. A connection that
17
+ several components share is a Resource, which the engine opens and closes for them.
18
+ """
19
+
20
+ from openrois.components.core.component import CommandFailed, Component, EngineBinding
21
+ from openrois.components.core.decorators import (
22
+ component,
23
+ invoke,
24
+ on_set_parameter,
25
+ query,
26
+ subscribe,
27
+ )
28
+ from openrois.components.core.resource import Resource, ResourceBinding
29
+
30
+ __all__ = [
31
+ "CommandFailed",
32
+ "Component",
33
+ "EngineBinding",
34
+ "Resource",
35
+ "ResourceBinding",
36
+ "component",
37
+ "invoke",
38
+ "on_set_parameter",
39
+ "query",
40
+ "subscribe",
41
+ ]
@@ -0,0 +1,180 @@
1
+ """What a decorated component class declares, checked against its profile.
2
+
3
+ ``@component`` builds a ComponentSpec when a class is defined: which method handles
4
+ each command, query and event, the hook for new parameter values, and the profile the
5
+ engine serves for the class. A handler that does not match the profile fails here, at
6
+ class definition, so a typo cannot advertise a message the profile does not define.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import inspect
12
+ from collections.abc import Mapping
13
+ from dataclasses import dataclass
14
+ from types import MappingProxyType
15
+ from typing import Literal
16
+
17
+ from openrois.interfaces.components import ROIS_COMMON_PROFILE, ROIS_COMMON_URN
18
+ from openrois.interfaces.profiles import (
19
+ CommandMessageProfile,
20
+ EventMessageProfile,
21
+ HRIComponentProfile,
22
+ QueryMessageProfile,
23
+ )
24
+
25
+ type HandlerKind = Literal["command", "query", "event", "set_parameter"]
26
+
27
+ # The messages whose handlers take arguments by name.
28
+ type MessageWithArguments = CommandMessageProfile | QueryMessageProfile | EventMessageProfile
29
+
30
+ # The attribute the method decorators set on the functions they mark.
31
+ HANDLER_ATTRIBUTE = "__rois_handler__"
32
+
33
+ # The engine answers these itself, so a component never handles them.
34
+ ENGINE_COMMANDS = frozenset({"set_parameter"})
35
+ ENGINE_QUERIES = frozenset({"component_status"})
36
+
37
+ # A component whose profile includes RoISCommon serves these, handler or not.
38
+ ROIS_COMMON_COMMANDS = frozenset(m.name for m in ROIS_COMMON_PROFILE.command_profiles)
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class Handler:
43
+ """The message a method handles, set on the method by a decorator."""
44
+
45
+ kind: HandlerKind
46
+ name: str
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class ComponentSpec:
51
+ """The messages a component class handles and the profile it serves.
52
+
53
+ Attributes:
54
+ declared: The profile the class declares, usually a profile constant.
55
+ served: The declared profile with only the messages the class answers: the
56
+ commands, queries and events it has handlers for, the four RoIS_Common
57
+ commands when the profile includes RoISCommon (the engine answers them by
58
+ default), otherwise ``stop`` when it handles ``start``, and
59
+ ``component_status``, which the engine answers. Every parameter of the
60
+ declared profile stays.
61
+ commands: The method that handles each command, by command name.
62
+ queries: The method that handles each query, by query name.
63
+ events: The method that runs on a new subscription, by event name.
64
+ set_parameter_hook: The method that applies new parameter values, if any.
65
+ """
66
+
67
+ declared: HRIComponentProfile
68
+ served: HRIComponentProfile
69
+ commands: Mapping[str, str]
70
+ queries: Mapping[str, str]
71
+ events: Mapping[str, str]
72
+ set_parameter_hook: str | None
73
+
74
+
75
+ def build_spec(cls: type, profile: HRIComponentProfile) -> ComponentSpec:
76
+ """Collect the handlers of a class and check them against its profile.
77
+
78
+ Raises:
79
+ TypeError: A handler is not a coroutine function or has parameters its message
80
+ does not define.
81
+ ValueError: A handler names a message the profile does not define, a message the
82
+ engine answers itself, or a message another handler already handles.
83
+ """
84
+ owner = f"{cls.__qualname__}"
85
+ kind_of_type = f"{profile.identifier.authority} {profile.identifier.code}".strip()
86
+ defined: dict[HandlerKind, set[str]] = {
87
+ "command": {m.name for m in profile.command_profiles},
88
+ "query": {m.name for m in profile.query_profiles},
89
+ "event": {m.name for m in profile.event_profiles},
90
+ }
91
+ tables: dict[HandlerKind, dict[str, str]] = {"command": {}, "query": {}, "event": {}}
92
+ hook: str | None = None
93
+
94
+ for attribute in dir(cls):
95
+ method = getattr(cls, attribute, None)
96
+ handler = getattr(method, HANDLER_ATTRIBUTE, None)
97
+ if not isinstance(handler, Handler):
98
+ continue
99
+ where = f"{owner}.{attribute}"
100
+ if not inspect.iscoroutinefunction(method):
101
+ raise TypeError(f"{where} must be an async def")
102
+ parameters = list(inspect.signature(method).parameters.values())[1:]
103
+
104
+ if handler.kind == "set_parameter":
105
+ if not profile.parameter_profiles:
106
+ raise ValueError(f"{where} is an @on_set_parameter hook, but the profile "
107
+ f"{kind_of_type} declares no parameters")
108
+ if hook is not None:
109
+ raise ValueError(f"{owner} has two @on_set_parameter hooks: {hook} and "
110
+ f"{attribute}")
111
+ if len(parameters) != 1:
112
+ raise TypeError(f"{where} must take one argument, the new values")
113
+ hook = attribute
114
+ continue
115
+
116
+ if handler.kind == "command" and handler.name in ENGINE_COMMANDS:
117
+ raise ValueError(f"{where} handles {handler.name}, which the engine runs itself. "
118
+ "Apply new parameter values in an @on_set_parameter hook")
119
+ if handler.kind == "query" and handler.name in ENGINE_QUERIES:
120
+ raise ValueError(f"{where} answers {handler.name}, which the engine answers "
121
+ "itself")
122
+ if handler.name not in defined[handler.kind]:
123
+ raise ValueError(f"{where} handles the {handler.kind} {handler.name!r}, which the "
124
+ f"profile {kind_of_type} does not define")
125
+ table = tables[handler.kind]
126
+ if handler.name in table:
127
+ raise ValueError(f"{owner}.{table[handler.name]} and {where} both handle the "
128
+ f"{handler.kind} {handler.name!r}")
129
+ message: MessageWithArguments
130
+ if handler.kind == "command":
131
+ message = next(m for m in profile.command_profiles if m.name == handler.name)
132
+ elif handler.kind == "query":
133
+ message = next(m for m in profile.query_profiles if m.name == handler.name)
134
+ else:
135
+ message = next(m for m in profile.event_profiles if m.name == handler.name)
136
+ _check_parameters(where, message, parameters)
137
+ table[handler.name] = attribute
138
+
139
+ commands, queries, events = tables["command"], tables["query"], tables["event"]
140
+ rois_common = ROIS_COMMON_URN in profile.sub_component_profiles
141
+ served = profile.model_copy(
142
+ update={
143
+ "command_profiles": [
144
+ m
145
+ for m in profile.command_profiles
146
+ if m.name in commands
147
+ or (rois_common and m.name in ROIS_COMMON_COMMANDS)
148
+ or (m.name == "stop" and "start" in commands)
149
+ ],
150
+ "query_profiles": [
151
+ m for m in profile.query_profiles if m.name in queries or m.name in ENGINE_QUERIES
152
+ ],
153
+ "event_profiles": [m for m in profile.event_profiles if m.name in events],
154
+ }
155
+ )
156
+ return ComponentSpec(
157
+ declared=profile,
158
+ served=served,
159
+ commands=MappingProxyType(commands),
160
+ queries=MappingProxyType(queries),
161
+ events=MappingProxyType(events),
162
+ set_parameter_hook=hook,
163
+ )
164
+
165
+
166
+ def _check_parameters(
167
+ where: str,
168
+ message: MessageWithArguments,
169
+ parameters: list[inspect.Parameter],
170
+ ) -> None:
171
+ """A handler takes the arguments of its message by name, and nothing else."""
172
+ arguments = {a.name for a in message.arguments}
173
+ for parameter in parameters:
174
+ if parameter.kind is inspect.Parameter.VAR_KEYWORD:
175
+ continue
176
+ if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
177
+ raise TypeError(f"{where} cannot take *{parameter.name}: arguments arrive by name")
178
+ if parameter.name not in arguments:
179
+ raise TypeError(f"{where} takes {parameter.name!r}, which the message "
180
+ f"{message.name!r} does not define as an argument")
@@ -0,0 +1,412 @@
1
+ """The Component base class and what it needs from the engine that hosts it.
2
+
3
+ A component is a subclass of Component declared with ``@component(profile)``. Its author
4
+ writes handlers for the messages it supports and uses what the engine binds at
5
+ registration: ``self.parameters``, the current parameter values, ``self.emit``, which
6
+ sends an event to the clients that subscribed, and ``self.raise_status`` and
7
+ ``self.clear_status``, which report WARNING or ERROR.
8
+
9
+ The methods whose names start with ``rois_`` are the side the engine calls. An engine
10
+ that hosts components written with this SDK calls them to read the served profile, bind
11
+ itself, run commands and queries, start subscriptions and apply new parameter values.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Mapping, Sequence
17
+ from types import MappingProxyType
18
+ from typing import Any, ClassVar, Protocol
19
+
20
+ from openrois.interfaces.common import ComponentStatus
21
+ from openrois.interfaces.hri import Argument, Parameter, Result
22
+ from openrois.interfaces.profiles import (
23
+ CommandMessageProfile,
24
+ EventMessageProfile,
25
+ HRIComponentProfile,
26
+ MessageProfile,
27
+ QueryMessageProfile,
28
+ )
29
+ from openrois.interfaces.service import CompletedStatus
30
+ from openrois.interfaces.values import decode_value, encode_value
31
+
32
+ from openrois.components.core._spec import ComponentSpec
33
+ from openrois.components.core.resource import Resource, check_raisable
34
+
35
+
36
+ class CommandFailed(Exception): # noqa: N818, the name says what happened to the command.
37
+ """Raised by a command handler to end its command with a status other than OK.
38
+
39
+ Any other exception ends the command with ERROR.
40
+
41
+ Attributes:
42
+ status: How the command ended: ERROR, ABORT, OUT_OF_RESOURCES or TIMEOUT.
43
+ """
44
+
45
+ def __init__(self, status: CompletedStatus = CompletedStatus.ERROR, message: str = "") -> None:
46
+ if status is CompletedStatus.OK:
47
+ raise ValueError("A failed command cannot end with OK")
48
+ super().__init__(message or status.value)
49
+ self.status = status
50
+
51
+
52
+ class EngineBinding(Protocol):
53
+ """What a component needs from the engine that hosts it.
54
+
55
+ The engine passes an object of this shape to ``Component.rois_bind`` when it
56
+ registers the component.
57
+ """
58
+
59
+ @property
60
+ def ref(self) -> str:
61
+ """The fully qualified ref of the component, ``engine_id/name``."""
62
+ ...
63
+
64
+ def parameters(self) -> Sequence[Parameter]:
65
+ """The current value of every parameter of the component."""
66
+ ...
67
+
68
+ def emit(
69
+ self,
70
+ event_type: str,
71
+ results: Sequence[Result],
72
+ concerning: Mapping[str, str] | None = None,
73
+ ) -> None:
74
+ """Send an event to its subscriptions. Safe to call from any thread.
75
+
76
+ ``concerning`` names the argument values the event is about, in their string
77
+ form. The event goes to the subscriptions whose arguments equal them, and to
78
+ every subscription of the event when it is empty.
79
+ """
80
+ ...
81
+
82
+ def raise_status(self, status: ComponentStatus, message: str) -> None:
83
+ """Report WARNING or ERROR for the component. Safe to call from any thread."""
84
+ ...
85
+
86
+ def clear_status(self) -> None:
87
+ """Withdraw the status the component raised. Safe to call from any thread."""
88
+ ...
89
+
90
+ def engine_status(self) -> ComponentStatus:
91
+ """The status of the engine: ERROR or WARNING as raised on it, or READY."""
92
+ ...
93
+
94
+
95
+ class Component:
96
+ """Base class of a RoIS HRI Component.
97
+
98
+ Subclass it, declare the profile with ``@component``, and write handlers::
99
+
100
+ @component(NAVIGATION_PROFILE)
101
+ class Navigation(Component):
102
+ @invoke("start")
103
+ async def start(self) -> dict[str, object]:
104
+ target = self.parameters["target_positions"][0]
105
+ await self._robot.go_to(target)
106
+ self.emit("reached_target", target=target, is_final_target=True)
107
+ return {}
108
+
109
+ @subscribe("reached_target")
110
+ async def reached_target(self) -> None:
111
+ pass # start emits the event when the robot arrives.
112
+
113
+ A command handler runs for as long as the command lasts. It takes the arguments of
114
+ its command by name, converted from their profile types, and returns the values of
115
+ the command's results by name, or None. Raising CommandFailed ends the command with
116
+ the status it names, and any other exception ends it with ERROR. A query handler
117
+ returns the values of the query's results the same way.
118
+
119
+ The engine calls ``connect()`` before the component takes requests and
120
+ ``disconnect()`` when it stops. Override them to open and close the connection to
121
+ the backend. A connection that several components share is a Resource: the
122
+ component records it with ``self.uses(resource)``, and the engine opens it before
123
+ the first of them connects.
124
+ """
125
+
126
+ # Set by @component on the class it declares.
127
+ __rois_spec__: ClassVar[ComponentSpec | None] = None
128
+
129
+ # Set by rois_bind when the engine registers the component.
130
+ _rois_binding: EngineBinding | None = None
131
+
132
+ # Set by uses(), in the order the component recorded them.
133
+ _rois_resources: list[Resource] | None = None
134
+
135
+ # -- For component authors ---------------------------------------------------------
136
+
137
+ @property
138
+ def ref(self) -> str:
139
+ """The fully qualified ref the engine registered the component under."""
140
+ return self._binding().ref
141
+
142
+ @property
143
+ def parameters(self) -> Mapping[str, Any]:
144
+ """The current parameter values, converted from their profile types.
145
+
146
+ A ``string[]`` parameter reads as a list of str and an ``int`` as an int. The
147
+ engine stores the values and changes them on ``set_parameter``. A parameter with
148
+ no value yet, because its profile gives no default and no client has set it, is
149
+ missing from the mapping.
150
+ """
151
+ types = {p.name: p.data_type_ref.code for p in self._spec().declared.parameter_profiles}
152
+ return MappingProxyType(
153
+ {
154
+ p.name: decode_value(types.get(p.name, p.data_type_ref), p.value)
155
+ for p in self._binding().parameters()
156
+ if p.value != ""
157
+ }
158
+ )
159
+
160
+ def emit(
161
+ self,
162
+ event_type: str,
163
+ *,
164
+ concerning: Mapping[str, object] | None = None,
165
+ **values: object,
166
+ ) -> None:
167
+ """Send an event to the clients that subscribed to it.
168
+
169
+ The values are the event's results by name, converted to their profile types. A
170
+ result left out is not sent. Safe to call from any thread, for example a ROS 2
171
+ callback.
172
+
173
+ An event whose profile has arguments, such as the ``stream_id`` of
174
+ ``notify_stream_status``, names in ``concerning`` the argument values it is
175
+ about. It then reaches the subscriptions that gave those values, or none, and an
176
+ event that names no values reaches every subscription.
177
+
178
+ Raises:
179
+ ValueError: The component does not serve the event, a value has no result
180
+ of that name or does not fit its type, or ``concerning`` names an
181
+ argument the event does not have.
182
+ RuntimeError: No engine hosts the component yet.
183
+ """
184
+ spec = self._spec()
185
+ if event_type not in spec.events:
186
+ raise ValueError(
187
+ f"{type(self).__qualname__} does not serve the event {event_type!r}. "
188
+ "Mark it with @subscribe"
189
+ )
190
+ message = _message(spec.served.event_profiles, event_type)
191
+ concerns = _concerns(message, concerning or {})
192
+ self._binding().emit(event_type, _results(message, values), concerns)
193
+
194
+ def uses[R: Resource](self, resource: R) -> R:
195
+ """Record a resource the component shares with others, and return it.
196
+
197
+ Call it in ``__init__``. The engine opens every resource a component uses before
198
+ the component connects, and a status the resource raises becomes the
199
+ component's.
200
+ """
201
+ if self._rois_resources is None:
202
+ self._rois_resources = []
203
+ if not any(r is resource for r in self._rois_resources):
204
+ self._rois_resources.append(resource)
205
+ return resource
206
+
207
+ def raise_status(self, status: ComponentStatus, message: str = "") -> None:
208
+ """Report WARNING or ERROR, with a message, until ``clear_status``.
209
+
210
+ component_status reports the raised status, and the engine sends
211
+ ``rois.system.notify_error`` to the sessions that bound the component or
212
+ subscribed to its events. While ERROR is raised, the engine refuses the
213
+ component's commands and queries and holds back its events. Safe to call from
214
+ any thread.
215
+
216
+ Raises:
217
+ ValueError: The status is neither WARNING nor ERROR.
218
+ RuntimeError: No engine hosts the component yet.
219
+ """
220
+ check_raisable(status)
221
+ self._binding().raise_status(status, message)
222
+
223
+ def clear_status(self) -> None:
224
+ """Withdraw the status the component raised. Safe to call from any thread.
225
+
226
+ Raises:
227
+ RuntimeError: No engine hosts the component yet.
228
+ """
229
+ self._binding().clear_status()
230
+
231
+ def engine_state(self) -> ComponentStatus:
232
+ """The status of the engine that hosts the component.
233
+
234
+ ERROR or WARNING while one is raised on the engine, READY otherwise. A
235
+ SystemInformation component answers ``engine_status`` with it.
236
+
237
+ Raises:
238
+ RuntimeError: No engine hosts the component yet.
239
+ """
240
+ return self._binding().engine_status()
241
+
242
+ async def connect(self) -> None:
243
+ """Open the connection to the backend. The default does nothing."""
244
+
245
+ async def disconnect(self) -> None:
246
+ """Close the connection to the backend. The default does nothing."""
247
+
248
+ # -- Called by the engine ----------------------------------------------------------
249
+
250
+ @classmethod
251
+ def rois_profile(cls) -> HRIComponentProfile:
252
+ """The profile the engine serves for this class: what it implements."""
253
+ return cls._spec().served
254
+
255
+ def rois_bind(self, binding: EngineBinding) -> None:
256
+ """Attach the engine that hosts this component."""
257
+ self._rois_binding = binding
258
+
259
+ def rois_resources(self) -> Sequence[Resource]:
260
+ """The resources the component uses, in the order it recorded them."""
261
+ return list(self._rois_resources or ())
262
+
263
+ async def rois_command(
264
+ self,
265
+ command_type: str,
266
+ arguments: Sequence[Argument] = (),
267
+ ) -> list[Result]:
268
+ """Run one command until it completes, and return its results.
269
+
270
+ A served command without a handler returns at once. That is ``stop`` alongside
271
+ ``start``, and the four RoIS_Common commands of a profile that includes
272
+ RoISCommon: the engine holds, releases and aborts commands itself, and a start
273
+ without a handler is done once it begins.
274
+
275
+ Raises:
276
+ CommandFailed: The handler ended the command with another status.
277
+ ValueError: The component does not serve the command, or an argument does
278
+ not fit its profile.
279
+ """
280
+ spec = self._spec()
281
+ message = _message(spec.served.command_profiles, command_type)
282
+ method = spec.commands.get(command_type)
283
+ if method is None:
284
+ return []
285
+ values = await getattr(self, method)(**_arguments(message, arguments))
286
+ return _results(message, values)
287
+
288
+ async def rois_query(
289
+ self,
290
+ query_type: str,
291
+ arguments: Sequence[Argument] = (),
292
+ ) -> list[Result]:
293
+ """Answer one query with its results.
294
+
295
+ Raises:
296
+ ValueError: The component has no handler for the query, or an argument does
297
+ not fit its profile.
298
+ """
299
+ spec = self._spec()
300
+ method = spec.queries.get(query_type)
301
+ if method is None:
302
+ raise ValueError(f"{type(self).__qualname__} does not answer {query_type!r}")
303
+ message = _message(spec.served.query_profiles, query_type)
304
+ values = await getattr(self, method)(**_arguments(message, arguments))
305
+ return _results(message, values)
306
+
307
+ async def rois_subscribed(self, event_type: str, arguments: Sequence[Argument] = ()) -> None:
308
+ """Run the @subscribe method of an event for a new subscription.
309
+
310
+ The method receives the arguments the subscription gave, by name.
311
+
312
+ Raises:
313
+ ValueError: The component does not serve the event, or an argument does not
314
+ fit its profile.
315
+ """
316
+ spec = self._spec()
317
+ method = spec.events.get(event_type)
318
+ if method is None:
319
+ raise ValueError(f"{type(self).__qualname__} does not serve {event_type!r}")
320
+ message = _message(spec.served.event_profiles, event_type)
321
+ await getattr(self, method)(**_arguments(message, arguments))
322
+
323
+ async def rois_set_parameters(self, parameters: Sequence[Parameter]) -> None:
324
+ """Run the @on_set_parameter hook with new values, before the engine stores them.
325
+
326
+ Without a hook this returns at once. An exception from the hook refuses the
327
+ values, and the engine keeps the old ones.
328
+
329
+ Raises:
330
+ ValueError: A parameter is not in the profile or its value does not fit.
331
+ """
332
+ spec = self._spec()
333
+ if spec.set_parameter_hook is None:
334
+ return
335
+ types = {p.name: p.data_type_ref.code for p in spec.declared.parameter_profiles}
336
+ values: dict[str, Any] = {}
337
+ for parameter in parameters:
338
+ if parameter.name not in types:
339
+ raise ValueError(f"The profile defines no parameter {parameter.name!r}")
340
+ values[parameter.name] = decode_value(types[parameter.name], parameter.value)
341
+ await getattr(self, spec.set_parameter_hook)(MappingProxyType(values))
342
+
343
+ # -- Internals ---------------------------------------------------------------------
344
+
345
+ @classmethod
346
+ def _spec(cls) -> ComponentSpec:
347
+ spec = cls.__rois_spec__
348
+ if spec is None:
349
+ raise TypeError(f"{cls.__qualname__} is not declared with @component(profile)")
350
+ return spec
351
+
352
+ def _binding(self) -> EngineBinding:
353
+ if self._rois_binding is None:
354
+ raise RuntimeError(f"No engine hosts this {type(self).__qualname__} yet")
355
+ return self._rois_binding
356
+
357
+
358
+ def _message[M: MessageProfile](messages: Sequence[M], name: str) -> M:
359
+ """The message profile of one name, among the served ones."""
360
+ for message in messages:
361
+ if message.name == name:
362
+ return message
363
+ raise ValueError(f"The component does not serve {name!r}")
364
+
365
+
366
+ type _WithArguments = CommandMessageProfile | QueryMessageProfile | EventMessageProfile
367
+
368
+
369
+ def _arguments(message: _WithArguments, arguments: Sequence[Argument]) -> dict[str, Any]:
370
+ """The arguments of a message by name, converted from their profile types."""
371
+ types = {a.name: a.data_type_ref.code for a in message.arguments}
372
+ values: dict[str, Any] = {}
373
+ for argument in arguments:
374
+ if argument.name not in types:
375
+ raise ValueError(f"{message.name!r} has no argument {argument.name!r}")
376
+ values[argument.name] = decode_value(types[argument.name], argument.value)
377
+ return values
378
+
379
+
380
+ def _concerns(message: EventMessageProfile, values: Mapping[str, object]) -> dict[str, str]:
381
+ """The argument values an event concerns, in the form their types take on the wire."""
382
+ types = {a.name: a.data_type_ref.code for a in message.arguments}
383
+ concerns: dict[str, str] = {}
384
+ for name, value in values.items():
385
+ if name not in types:
386
+ raise ValueError(f"{message.name!r} has no argument {name!r}")
387
+ concerns[name] = encode_value(types[name], value)
388
+ return concerns
389
+
390
+
391
+ def _results(message: MessageProfile, values: object) -> list[Result]:
392
+ """The results of a message from their values by name, in the order of the profile."""
393
+ if values is None:
394
+ return []
395
+ if not isinstance(values, Mapping):
396
+ raise TypeError(
397
+ f"A handler of {message.name!r} returns its result values by name, or None, "
398
+ f"not {type(values).__name__}"
399
+ )
400
+ declared = {r.name for r in message.results}
401
+ unknown = sorted(set(values) - declared)
402
+ if unknown:
403
+ raise ValueError(f"{message.name!r} has no result named {', '.join(unknown)}")
404
+ return [
405
+ Result(
406
+ name=r.name,
407
+ data_type_ref=r.data_type_ref.code,
408
+ value=encode_value(r.data_type_ref.code, values[r.name]),
409
+ )
410
+ for r in message.results
411
+ if r.name in values
412
+ ]