python-statemachine 3.1.2__py3-none-any.whl → 3.2.1__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 (60) hide show
  1. {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/METADATA +16 -3
  2. python_statemachine-3.2.1.dist-info/RECORD +72 -0
  3. {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/WHEEL +1 -1
  4. statemachine/__init__.py +1 -1
  5. statemachine/callbacks.py +5 -11
  6. statemachine/configuration.py +5 -6
  7. statemachine/contrib/diagram/extract.py +26 -27
  8. statemachine/contrib/diagram/formatter.py +5 -7
  9. statemachine/contrib/diagram/model.py +9 -11
  10. statemachine/contrib/diagram/renderers/dot.py +20 -26
  11. statemachine/contrib/diagram/renderers/mermaid.py +36 -40
  12. statemachine/contrib/diagram/renderers/table.py +7 -9
  13. statemachine/contrib/weighted.py +7 -11
  14. statemachine/dispatcher.py +18 -13
  15. statemachine/engines/async_.py +20 -9
  16. statemachine/engines/base.py +26 -17
  17. statemachine/event.py +1 -2
  18. statemachine/exceptions.py +2 -2
  19. statemachine/factory.py +11 -15
  20. statemachine/graph.py +2 -2
  21. statemachine/invoke.py +12 -11
  22. statemachine/io/__init__.py +52 -225
  23. statemachine/io/actions.py +627 -0
  24. statemachine/io/builder.py +195 -0
  25. statemachine/io/class_factory.py +236 -0
  26. statemachine/io/evaluators.py +303 -0
  27. statemachine/io/interpreter.py +128 -0
  28. statemachine/io/{scxml/invoke.py → invoke.py} +89 -49
  29. statemachine/io/json/__init__.py +1 -0
  30. statemachine/io/json/reader.py +27 -0
  31. statemachine/io/loader.py +175 -0
  32. statemachine/io/model.py +268 -0
  33. statemachine/io/native.py +402 -0
  34. statemachine/io/ports.py +83 -0
  35. statemachine/io/schemas/statechart.schema.json +258 -0
  36. statemachine/io/scxml/__init__.py +15 -0
  37. statemachine/io/scxml/processor.py +23 -253
  38. statemachine/io/scxml/{parser.py → reader.py} +88 -56
  39. statemachine/io/system_variables.py +199 -0
  40. statemachine/io/validation.py +44 -0
  41. statemachine/io/yaml/__init__.py +1 -0
  42. statemachine/io/yaml/reader.py +65 -0
  43. statemachine/locale/en/LC_MESSAGES/statemachine.po +19 -19
  44. statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +19 -19
  45. statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +19 -19
  46. statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +19 -19
  47. statemachine/orderedset.py +15 -6
  48. statemachine/registry.py +1 -4
  49. statemachine/signature.py +2 -5
  50. statemachine/spec_parser.py +228 -45
  51. statemachine/state.py +7 -8
  52. statemachine/statemachine.py +18 -20
  53. statemachine/states.py +3 -5
  54. statemachine/transition.py +3 -4
  55. statemachine/transition_list.py +4 -5
  56. statemachine/transition_mixin.py +1 -1
  57. python_statemachine-3.1.2.dist-info/RECORD +0 -58
  58. statemachine/io/scxml/actions.py +0 -650
  59. statemachine/io/scxml/schema.py +0 -175
  60. {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,650 +0,0 @@
1
- import html
2
- import logging
3
- import re
4
- from dataclasses import dataclass
5
- from itertools import chain
6
- from typing import Any
7
- from typing import Callable
8
- from uuid import uuid4
9
-
10
- from ...event import BoundEvent
11
- from ...event import Event
12
- from ...event import _event_data_kwargs
13
- from ...spec_parser import InState
14
- from ...statemachine import StateChart
15
- from .parser import Action
16
- from .parser import AssignAction
17
- from .parser import IfAction
18
- from .parser import LogAction
19
- from .parser import RaiseAction
20
- from .parser import SendAction
21
- from .schema import CancelAction
22
- from .schema import DataItem
23
- from .schema import DataModel
24
- from .schema import DoneData
25
- from .schema import ExecutableContent
26
- from .schema import ForeachAction
27
- from .schema import Param
28
- from .schema import ScriptAction
29
-
30
- logger = logging.getLogger(__name__)
31
- _debug = logger.debug if logger.isEnabledFor(logging.DEBUG) else lambda *a, **k: None
32
- protected_attrs = _event_data_kwargs | {"_sessionid", "_ioprocessors", "_name", "_event"}
33
-
34
-
35
- class ParseTime:
36
- pattern = re.compile(r"(\d+)?(\.\d+)?(s|ms)")
37
-
38
- @classmethod
39
- def parse_delay(cls, delay: "str | None", delayexpr: "str | None", **kwargs):
40
- if delay:
41
- return cls.time_in_ms(delay)
42
- elif delayexpr:
43
- delay_expr_expanded = cls.replace(delayexpr)
44
- return cls.time_in_ms(_eval(delay_expr_expanded, **kwargs))
45
-
46
- return 0
47
-
48
- @classmethod
49
- def replace(cls, expr: str) -> str:
50
- def rep(match):
51
- return str(cls.time_in_ms(match.group(0)))
52
-
53
- return cls.pattern.sub(rep, expr)
54
-
55
- @classmethod
56
- def time_in_ms(cls, expr: str) -> float:
57
- """
58
- Convert a CSS2 time expression to milliseconds.
59
-
60
- Args:
61
- time (str): A string representing the time, e.g., '1.5s' or '150ms'.
62
-
63
- Returns:
64
- float: The time in milliseconds.
65
-
66
- Raises:
67
- ValueError: If the input is not a valid CSS2 time expression.
68
- """
69
- if expr.endswith("ms"):
70
- try:
71
- return float(expr[:-2])
72
- except ValueError as e:
73
- raise ValueError(f"Invalid time value: {expr}") from e
74
- elif expr.endswith("s"):
75
- try:
76
- return float(expr[:-1]) * 1000
77
- except ValueError as e:
78
- raise ValueError(f"Invalid time value: {expr}") from e
79
- else:
80
- try:
81
- return float(expr)
82
- except ValueError as e:
83
- raise ValueError(f"Invalid time unit in: {expr}") from e
84
-
85
-
86
- @dataclass
87
- class _Data:
88
- kwargs: dict
89
-
90
- def __getattr__(self, name):
91
- return self.kwargs.get(name, None)
92
-
93
- def get(self, name, default=None):
94
- return self.kwargs.get(name, default)
95
-
96
-
97
- class OriginTypeSCXML(str):
98
- """The origintype of the :ref:`Event` as specified by the SCXML namespace."""
99
-
100
- def __eq__(self, other):
101
- return other == "http://www.w3.org/TR/scxml/#SCXMLEventProcessor" or other == "scxml"
102
-
103
-
104
- class EventDataWrapper:
105
- origin: str = ""
106
- origintype: str = OriginTypeSCXML("scxml")
107
- """The origintype of the :ref:`Event` as specified by the SCXML namespace."""
108
- invokeid: str = ""
109
- """If this event is generated from an invoked child process, the SCXML Processor MUST set
110
- this field to the invoke id of the invocation that triggered the child process.
111
- Otherwise it MUST leave it blank.
112
- """
113
-
114
- def __init__(self, event_data=None, *, trigger_data=None):
115
- self.event_data = event_data
116
- if trigger_data is not None:
117
- self.trigger_data = trigger_data
118
- elif event_data is not None:
119
- self.trigger_data = event_data.trigger_data
120
- else:
121
- raise ValueError("Either event_data or trigger_data must be provided")
122
-
123
- td = self.trigger_data
124
- self.sendid = td.send_id
125
- self.invokeid = td.kwargs.get("_invokeid", "")
126
- if td.event is None or td.event.internal:
127
- if "error.execution" == td.event:
128
- self.type = "platform"
129
- else:
130
- self.type = "internal"
131
- self.origintype = ""
132
- else:
133
- self.type = "external"
134
-
135
- @classmethod
136
- def from_trigger_data(cls, trigger_data):
137
- """Create an EventDataWrapper directly from a TriggerData (no EventData needed)."""
138
- return cls(trigger_data=trigger_data)
139
-
140
- def __getattr__(self, name):
141
- if self.event_data is not None:
142
- return getattr(self.event_data, name)
143
- raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
144
-
145
- def __eq__(self, value):
146
- "This makes SCXML test 329 pass. It assumes that the event is the same instance"
147
- return isinstance(value, EventDataWrapper)
148
-
149
- @property
150
- def name(self):
151
- if self.event_data is not None:
152
- return self.event_data.event
153
- return str(self.trigger_data.event) if self.trigger_data.event else None
154
-
155
- @property
156
- def data(self):
157
- "Property used by the SCXML namespace"
158
- td = self.trigger_data
159
- if td.kwargs:
160
- return _Data(td.kwargs)
161
- elif td.args and len(td.args) == 1:
162
- return td.args[0]
163
- elif td.args:
164
- return td.args
165
- else:
166
- return None
167
-
168
-
169
- def _eval(expr: str, **kwargs) -> Any:
170
- if "machine" in kwargs:
171
- kwargs.update(
172
- **{
173
- k: v
174
- for k, v in kwargs["machine"].model.__dict__.items()
175
- if k not in protected_attrs
176
- }
177
- )
178
- kwargs["In"] = InState(kwargs["machine"])
179
- return eval(expr, {}, kwargs)
180
-
181
-
182
- class CallableAction:
183
- action: Any
184
-
185
- def __init__(self):
186
- self.__qualname__ = f"{self.__class__.__module__}.{self.__class__.__name__}"
187
-
188
- def __call__(self, *args, **kwargs):
189
- raise NotImplementedError
190
-
191
- def __str__(self):
192
- return f"{self.action}"
193
-
194
- def __repr__(self):
195
- return f"{self.__class__.__name__}({self.action!r})"
196
-
197
- @property
198
- def __name__(self):
199
- return str(self)
200
-
201
- @property
202
- def __code__(self):
203
- return self.__call__.__code__
204
-
205
-
206
- class Cond(CallableAction):
207
- """Evaluates a condition like a predicate and returns True or False."""
208
-
209
- @classmethod
210
- def create(cls, cond: "str | None", processor=None):
211
- cond = cls._normalize(cond)
212
- if cond is None:
213
- return None
214
-
215
- return cls(cond, processor)
216
-
217
- def __init__(self, cond: str, processor=None):
218
- super().__init__()
219
- self.action = cond
220
- self.processor = processor
221
-
222
- def __call__(self, *args, **kwargs):
223
- result = _eval(self.action, **kwargs)
224
- _debug("Cond %s -> %s", self.action, result)
225
- return result
226
-
227
- @staticmethod
228
- def _normalize(cond: "str | None") -> "str | None":
229
- """
230
- Normalizes a JavaScript-like condition string to be compatible with Python's eval.
231
- """
232
- if cond is None:
233
- return None
234
-
235
- # Decode HTML entities, to allow XML syntax like `Var1<Var2`
236
- cond = html.unescape(cond)
237
-
238
- replacements = {
239
- "true": "True",
240
- "false": "False",
241
- "null": "None",
242
- "===": "==",
243
- "!==": "!=",
244
- "&&": "and",
245
- "||": "or",
246
- }
247
-
248
- # Use regex to replace each JavaScript-like token with its Python equivalent
249
- pattern = re.compile(r"\b(?:true|false|null)\b|===|!==|&&|\|\|")
250
- return pattern.sub(lambda match: replacements[match.group(0)], cond)
251
-
252
-
253
- def create_action_callable(action: Action) -> Callable:
254
- if isinstance(action, RaiseAction):
255
- return create_raise_action_callable(action)
256
- elif isinstance(action, AssignAction):
257
- return Assign(action)
258
- elif isinstance(action, LogAction):
259
- return Log(action)
260
- elif isinstance(action, IfAction):
261
- return create_if_action_callable(action)
262
- elif isinstance(action, ForeachAction):
263
- return create_foreach_action_callable(action)
264
- elif isinstance(action, SendAction):
265
- return create_send_action_callable(action)
266
- elif isinstance(action, CancelAction):
267
- return create_cancel_action_callable(action)
268
- elif isinstance(action, ScriptAction):
269
- return create_script_action_callable(action)
270
- else:
271
- raise ValueError(f"Unknown action type: {type(action)}")
272
-
273
-
274
- class Assign(CallableAction):
275
- def __init__(self, action: AssignAction):
276
- super().__init__()
277
- self.action = action
278
-
279
- def __call__(self, *args, **kwargs):
280
- machine: StateChart = kwargs["machine"]
281
- if self.action.child_xml is not None:
282
- value = self.action.child_xml
283
- else:
284
- value = _eval(self.action.expr, **kwargs)
285
-
286
- *path, attr = self.action.location.split(".")
287
- obj = machine.model
288
- for p in path:
289
- obj = getattr(obj, p)
290
-
291
- if not attr.isidentifier() or not (hasattr(obj, attr) or attr in kwargs):
292
- raise ValueError(
293
- f"<assign> 'location' must be a valid Python attribute name and must be declared, "
294
- f"got: {self.action.location}"
295
- )
296
- if attr in protected_attrs:
297
- raise ValueError(
298
- f"<assign> 'location' cannot assign to a protected attribute: "
299
- f"{self.action.location}"
300
- )
301
- setattr(obj, attr, value)
302
- _debug("Assign: %s = %r", self.action.location, value)
303
-
304
-
305
- class Log(CallableAction):
306
- def __init__(self, action: LogAction):
307
- super().__init__()
308
- self.action = action
309
-
310
- def __call__(self, *args, **kwargs):
311
- value = _eval(self.action.expr, **kwargs) if self.action.expr else None
312
-
313
- if self.action.label and self.action.expr is not None:
314
- msg = f"{self.action.label}: {value!r}"
315
- elif self.action.label:
316
- msg = f"{self.action.label}"
317
- else:
318
- msg = f"{value!r}"
319
- print(msg)
320
-
321
-
322
- def create_if_action_callable(action: IfAction) -> Callable:
323
- branches = [
324
- (
325
- Cond.create(branch.cond),
326
- [create_action_callable(action) for action in branch.actions],
327
- )
328
- for branch in action.branches
329
- ]
330
-
331
- def if_action(*args, **kwargs):
332
- machine: StateChart = kwargs["machine"]
333
- for cond, actions in branches:
334
- try:
335
- cond_result = not cond or cond(*args, **kwargs)
336
- except Exception as e:
337
- # SCXML spec: condition error → treat as false, queue error.execution.
338
- if machine.catch_errors_as_events:
339
- machine.send("error.execution", error=e, internal=True)
340
- cond_result = False
341
- else:
342
- raise
343
- if cond_result:
344
- for action in actions:
345
- action(*args, **kwargs)
346
- return
347
-
348
- if_action.action = action # type: ignore[attr-defined]
349
- return if_action
350
-
351
-
352
- def create_foreach_action_callable(action: ForeachAction) -> Callable:
353
- child_actions = [create_action_callable(act) for act in action.content.actions]
354
-
355
- def foreach_action(*args, **kwargs):
356
- machine: StateChart = kwargs["machine"]
357
- try:
358
- # Evaluate the array expression to get the iterable
359
- array = _eval(action.array, **kwargs)
360
- except Exception as e:
361
- raise ValueError(f"Error evaluating <foreach> 'array' expression: {e}") from e
362
-
363
- if not action.item.isidentifier():
364
- raise ValueError(
365
- f"<foreach> 'item' must be a valid Python attribute name, got: {action.item}"
366
- )
367
- for index, item in enumerate(array):
368
- # Assign the item and optionally the index
369
- setattr(machine.model, action.item, item)
370
- if action.index:
371
- setattr(machine.model, action.index, index)
372
-
373
- # Execute child actions
374
- for act in child_actions:
375
- act(*args, **kwargs)
376
-
377
- foreach_action.action = action # type: ignore[attr-defined]
378
- return foreach_action
379
-
380
-
381
- def create_raise_action_callable(action: RaiseAction) -> Callable:
382
- def raise_action(*args, **kwargs):
383
- machine: StateChart = kwargs["machine"]
384
-
385
- Event(id=action.event, internal=True, _sm=machine).put()
386
-
387
- raise_action.action = action # type: ignore[attr-defined]
388
- return raise_action
389
-
390
-
391
- def _send_to_parent(action: SendAction, **kwargs):
392
- """Route a <send target="#_parent"> to the parent machine via _invoke_session."""
393
- machine = kwargs["machine"]
394
- session = getattr(machine, "_invoke_session", None)
395
- if session is None:
396
- logger.warning(
397
- "<send target='#_parent'> ignored: machine %r has no _invoke_session",
398
- machine.name,
399
- )
400
- return
401
- event = action.event or _eval(action.eventexpr, **kwargs) # type: ignore[arg-type]
402
- names = []
403
- for name in (action.namelist or "").strip().split():
404
- if not hasattr(machine.model, name):
405
- raise NameError(f"Namelist variable '{name}' not found on model")
406
- names.append(Param(name=name, expr=name))
407
- params_values = {}
408
- for param in chain(names, action.params):
409
- if param.expr is None:
410
- continue
411
- params_values[param.name] = _eval(param.expr, **kwargs)
412
- session.send_to_parent(event, **params_values)
413
-
414
-
415
- def _send_to_invoke(action: SendAction, invokeid: str, **kwargs):
416
- """Route a <send target="#_<invokeid>"> to the invoked child session."""
417
- machine: StateChart = kwargs["machine"]
418
- event = action.event or _eval(action.eventexpr, **kwargs) # type: ignore[arg-type]
419
- names = []
420
- for name in (action.namelist or "").strip().split():
421
- if not hasattr(machine.model, name):
422
- raise NameError(f"Namelist variable '{name}' not found on model")
423
- names.append(Param(name=name, expr=name))
424
- params_values = {}
425
- for param in chain(names, action.params):
426
- if param.expr is None:
427
- continue
428
- params_values[param.name] = _eval(param.expr, **kwargs)
429
- if not machine._engine._invoke_manager.send_to_child(invokeid, event, **params_values):
430
- # Per SCXML spec: if target is not reachable → error.communication
431
- BoundEvent("error.communication", internal=True, _sm=machine).put()
432
-
433
-
434
- def create_send_action_callable(action: SendAction) -> Callable: # noqa: C901
435
- content: Any = ()
436
- _valid_targets = (None, "#_internal", "internal", "#_parent", "parent")
437
- if action.content:
438
- try:
439
- content = (eval(action.content, {}, {}),)
440
- except (NameError, SyntaxError, TypeError):
441
- content = (action.content,)
442
-
443
- def send_action(*args, **kwargs): # noqa: C901
444
- machine: StateChart = kwargs["machine"]
445
- event = action.event or _eval(action.eventexpr, **kwargs) # type: ignore[arg-type]
446
- target = action.target if action.target else None
447
-
448
- if action.type and action.type != "http://www.w3.org/TR/scxml/#SCXMLEventProcessor":
449
- # Per SCXML spec 6.2.3, unsupported type raises error.execution
450
- raise ValueError(
451
- f"Unsupported send type: {action.type}. "
452
- "Only 'http://www.w3.org/TR/scxml/#SCXMLEventProcessor' is supported"
453
- )
454
- if target not in _valid_targets:
455
- if target and target.startswith("#_scxml_"):
456
- # Valid SCXML session reference but undispatchable → error.communication
457
- BoundEvent("error.communication", internal=True, _sm=machine).put()
458
- elif target and target.startswith("#_"):
459
- # #_<invokeid> → route to invoked child session
460
- _send_to_invoke(action, target[2:], **kwargs)
461
- else:
462
- # Invalid target expression → error.execution (raised as exception)
463
- raise ValueError(f"Invalid target: {target}. Must be one of {_valid_targets}")
464
- return
465
-
466
- # Handle #_parent target — route to parent via _invoke_session
467
- if target == "#_parent":
468
- _send_to_parent(action, **kwargs)
469
- return
470
-
471
- internal = target in ("#_internal", "internal")
472
-
473
- send_id = None
474
- if action.id:
475
- send_id = action.id
476
- elif action.idlocation:
477
- send_id = uuid4().hex
478
- setattr(machine.model, action.idlocation, send_id)
479
-
480
- delay = ParseTime.parse_delay(action.delay, action.delayexpr, **kwargs)
481
-
482
- # Per SCXML spec, if namelist evaluation causes an error (e.g., variable not found),
483
- # the send MUST NOT be dispatched and error.execution is raised.
484
- names = []
485
- for name in (action.namelist or "").strip().split():
486
- if not hasattr(machine.model, name):
487
- raise NameError(f"Namelist variable '{name}' not found on model")
488
- names.append(Param(name=name, expr=name))
489
- params_values = {}
490
- for param in chain(names, action.params):
491
- if param.expr is None:
492
- continue
493
- params_values[param.name] = _eval(param.expr, **kwargs)
494
-
495
- Event(id=event, delay=delay, internal=internal, _sm=machine).put(
496
- *content,
497
- send_id=send_id,
498
- **params_values,
499
- )
500
-
501
- send_action.action = action # type: ignore[attr-defined]
502
- return send_action
503
-
504
-
505
- def create_cancel_action_callable(action: CancelAction) -> Callable:
506
- def cancel_action(*args, **kwargs):
507
- machine: StateChart = kwargs["machine"]
508
- if action.sendid:
509
- send_id = action.sendid
510
- elif action.sendidexpr:
511
- send_id = _eval(action.sendidexpr, **kwargs)
512
- else:
513
- raise ValueError("CancelAction must have either 'sendid' or 'sendidexpr'")
514
- # Implement cancel logic if necessary
515
- # For now, we can just print that the event is canceled
516
- machine.cancel_event(send_id)
517
-
518
- cancel_action.action = action # type: ignore[attr-defined]
519
- return cancel_action
520
-
521
-
522
- def create_script_action_callable(action: ScriptAction) -> Callable:
523
- def script_action(*args, **kwargs):
524
- machine: StateChart = kwargs["machine"]
525
- local_vars = {
526
- **machine.model.__dict__,
527
- }
528
- exec(action.content, {}, local_vars)
529
-
530
- # Assign the resulting variables to the state machine's model
531
- for var_name, value in local_vars.items():
532
- setattr(machine.model, var_name, value)
533
-
534
- script_action.action = action # type: ignore[attr-defined]
535
- return script_action
536
-
537
-
538
- def create_invoke_init_callable() -> Callable:
539
- """Create a callback that extracts invoke-specific kwargs and stores them on the machine.
540
-
541
- This is always inserted at position 0 in the initial state's onentry list by the
542
- SCXML processor, so that ``_invoke_session`` and ``_invoke_params`` are handled
543
- before any other callbacks run — even for SMs without a ``<datamodel>``.
544
- """
545
- initialized = False
546
-
547
- def invoke_init(*args, **kwargs):
548
- nonlocal initialized
549
- if initialized:
550
- return
551
- initialized = True
552
- machine = kwargs.get("machine")
553
- if machine is not None:
554
- # Use get() not pop(): each callback receives a copy of kwargs
555
- # (via EventData.extended_kwargs), so pop would be misleading.
556
- machine._invoke_params = kwargs.get("_invoke_params")
557
- machine._invoke_session = kwargs.get("_invoke_session")
558
-
559
- return invoke_init
560
-
561
-
562
- def _create_dataitem_callable(action: DataItem) -> Callable:
563
- def data_initializer(**kwargs):
564
- machine: StateChart = kwargs["machine"]
565
-
566
- # Check for invoke param overrides — params from parent override child defaults
567
- invoke_params = getattr(machine, "_invoke_params", None)
568
- if invoke_params and action.id in invoke_params:
569
- setattr(machine.model, action.id, invoke_params[action.id])
570
- return
571
-
572
- if action.expr:
573
- try:
574
- value = _eval(action.expr, **kwargs)
575
- except Exception:
576
- setattr(machine.model, action.id, None)
577
- raise
578
-
579
- elif action.content:
580
- try:
581
- value = _eval(action.content, **kwargs)
582
- except Exception:
583
- value = action.content
584
- else:
585
- value = None
586
-
587
- setattr(machine.model, action.id, value)
588
-
589
- return data_initializer
590
-
591
-
592
- def create_datamodel_action_callable(action: DataModel) -> "Callable | None":
593
- data_elements = [_create_dataitem_callable(item) for item in action.data]
594
- data_elements.extend([create_script_action_callable(script) for script in action.scripts])
595
-
596
- if not data_elements:
597
- return None
598
-
599
- initialized = False
600
-
601
- def datamodel(*args, **kwargs):
602
- nonlocal initialized
603
- if initialized:
604
- return
605
- initialized = True
606
-
607
- for act in data_elements:
608
- act(**kwargs)
609
-
610
- return datamodel
611
-
612
-
613
- class ExecuteBlock(CallableAction):
614
- """Parses the children as <executable> content XML into a callable."""
615
-
616
- def __init__(self, content: ExecutableContent):
617
- super().__init__()
618
- self.action = content
619
- self.action_callables = [create_action_callable(action) for action in content.actions]
620
-
621
- def __call__(self, *args, **kwargs):
622
- for action in self.action_callables:
623
- action(*args, **kwargs)
624
-
625
-
626
- class DoneDataCallable(CallableAction):
627
- """Evaluates <donedata> params/content and returns the data for done events."""
628
-
629
- def __init__(self, donedata: DoneData):
630
- super().__init__()
631
- self.action = donedata
632
- self.donedata = donedata
633
-
634
- def __call__(self, *args, **kwargs):
635
- if self.donedata.content_expr is not None:
636
- return _eval(self.donedata.content_expr, **kwargs)
637
-
638
- result = {}
639
- for param in self.donedata.params:
640
- if param.expr is not None:
641
- result[param.name] = _eval(param.expr, **kwargs)
642
- elif param.location is not None: # pragma: no branch
643
- location = param.location.strip()
644
- try:
645
- result[param.name] = _eval(location, **kwargs)
646
- except Exception as e:
647
- raise ValueError(
648
- f"<param> location '{location}' does not resolve to a valid value"
649
- ) from e
650
- return result