python-statemachine 3.1.1__py3-none-any.whl → 3.2.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.
- {python_statemachine-3.1.1.dist-info → python_statemachine-3.2.0.dist-info}/METADATA +16 -3
- python_statemachine-3.2.0.dist-info/RECORD +72 -0
- {python_statemachine-3.1.1.dist-info → python_statemachine-3.2.0.dist-info}/WHEEL +1 -1
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +5 -11
- statemachine/configuration.py +5 -6
- statemachine/contrib/diagram/__init__.py +15 -6
- statemachine/contrib/diagram/extract.py +23 -24
- statemachine/contrib/diagram/formatter.py +5 -7
- statemachine/contrib/diagram/model.py +9 -11
- statemachine/contrib/diagram/renderers/dot.py +20 -26
- statemachine/contrib/diagram/renderers/mermaid.py +36 -40
- statemachine/contrib/diagram/renderers/table.py +7 -9
- statemachine/contrib/weighted.py +7 -11
- statemachine/dispatcher.py +13 -12
- statemachine/engines/async_.py +5 -6
- statemachine/engines/base.py +12 -14
- statemachine/event.py +1 -2
- statemachine/exceptions.py +1 -1
- statemachine/factory.py +12 -16
- statemachine/graph.py +2 -2
- statemachine/invoke.py +12 -11
- statemachine/io/__init__.py +45 -225
- statemachine/io/{scxml/actions.py → actions.py} +158 -288
- statemachine/io/builder.py +195 -0
- statemachine/io/class_factory.py +236 -0
- statemachine/io/evaluators.py +275 -0
- statemachine/io/interpreter.py +128 -0
- statemachine/io/{scxml/invoke.py → invoke.py} +77 -49
- statemachine/io/json/__init__.py +1 -0
- statemachine/io/json/reader.py +27 -0
- statemachine/io/loader.py +161 -0
- statemachine/io/model.py +268 -0
- statemachine/io/native.py +402 -0
- statemachine/io/ports.py +83 -0
- statemachine/io/schemas/statechart.schema.json +258 -0
- statemachine/io/scxml/__init__.py +12 -0
- statemachine/io/scxml/processor.py +23 -253
- statemachine/io/scxml/{parser.py → reader.py} +64 -47
- statemachine/io/system_variables.py +184 -0
- statemachine/io/validation.py +44 -0
- statemachine/io/yaml/__init__.py +1 -0
- statemachine/io/yaml/reader.py +65 -0
- statemachine/locale/en/LC_MESSAGES/statemachine.po +19 -19
- statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +19 -19
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +19 -19
- statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +20 -22
- statemachine/orderedset.py +3 -3
- statemachine/registry.py +1 -4
- statemachine/signature.py +2 -5
- statemachine/spec_parser.py +171 -42
- statemachine/state.py +5 -6
- statemachine/statemachine.py +18 -20
- statemachine/states.py +3 -5
- statemachine/transition.py +3 -4
- statemachine/transition_list.py +4 -5
- statemachine/transition_mixin.py +1 -1
- python_statemachine-3.1.1.dist-info/RECORD +0 -58
- statemachine/io/scxml/schema.py +0 -175
- {python_statemachine-3.1.1.dist-info → python_statemachine-3.2.0.dist-info}/licenses/LICENSE +0 -0
statemachine/invoke.py
CHANGED
|
@@ -9,6 +9,7 @@ decorators (``@state.invoke``), and inline callables all work out of the box.
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import threading
|
|
11
11
|
import uuid
|
|
12
|
+
from collections.abc import Callable
|
|
12
13
|
from concurrent.futures import Future
|
|
13
14
|
from concurrent.futures import ThreadPoolExecutor
|
|
14
15
|
from dataclasses import dataclass
|
|
@@ -16,10 +17,6 @@ from dataclasses import field
|
|
|
16
17
|
from inspect import iscoroutinefunction
|
|
17
18
|
from typing import TYPE_CHECKING
|
|
18
19
|
from typing import Any
|
|
19
|
-
from typing import Callable
|
|
20
|
-
from typing import Dict
|
|
21
|
-
from typing import List
|
|
22
|
-
from typing import Tuple
|
|
23
20
|
from typing import runtime_checkable
|
|
24
21
|
|
|
25
22
|
try:
|
|
@@ -216,13 +213,13 @@ class InvokeGroup:
|
|
|
216
213
|
the exception propagates (which causes an ``error.execution`` event).
|
|
217
214
|
"""
|
|
218
215
|
|
|
219
|
-
def __init__(self, callables: "
|
|
216
|
+
def __init__(self, callables: "list[Callable[..., Any]]"):
|
|
220
217
|
self._callables = list(callables)
|
|
221
|
-
self._futures: "
|
|
218
|
+
self._futures: "list[Future[Any]]" = []
|
|
222
219
|
self._executor: "ThreadPoolExecutor | None" = None
|
|
223
220
|
|
|
224
|
-
def run(self, ctx: "InvokeContext") -> "
|
|
225
|
-
results: "
|
|
221
|
+
def run(self, ctx: "InvokeContext") -> "list[Any]":
|
|
222
|
+
results: "list[Any]" = [None] * len(self._callables)
|
|
226
223
|
self._executor = ThreadPoolExecutor(max_workers=len(self._callables))
|
|
227
224
|
try:
|
|
228
225
|
self._futures = [self._executor.submit(fn) for fn in self._callables]
|
|
@@ -282,8 +279,8 @@ class InvokeManager:
|
|
|
282
279
|
|
|
283
280
|
def __init__(self, engine: "BaseEngine"):
|
|
284
281
|
self._engine = engine
|
|
285
|
-
self._active:
|
|
286
|
-
self._pending: "
|
|
282
|
+
self._active: dict[str, Invocation] = {}
|
|
283
|
+
self._pending: "list[tuple[State, dict]]" = []
|
|
287
284
|
|
|
288
285
|
@property
|
|
289
286
|
def _debug(self):
|
|
@@ -529,7 +526,11 @@ class InvokeManager:
|
|
|
529
526
|
self._debug("%s Error in on_cancel for %s", self._log_id, invokeid, exc_info=True)
|
|
530
527
|
|
|
531
528
|
# 3) Cancel the async task (raises CancelledError at next await).
|
|
532
|
-
if
|
|
529
|
+
if (
|
|
530
|
+
invocation.task is not None
|
|
531
|
+
and invocation.task is not asyncio.current_task()
|
|
532
|
+
and not invocation.task.done()
|
|
533
|
+
):
|
|
533
534
|
invocation.task.cancel()
|
|
534
535
|
|
|
535
536
|
# 4) Wait for the sync thread to actually finish (skip if we ARE
|
statemachine/io/__init__.py
CHANGED
|
@@ -1,225 +1,45 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
from
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
enter: "str | ActionProtocol | Sequence[str] | Sequence[ActionProtocol]"
|
|
47
|
-
exit: "str | ActionProtocol | Sequence[str] | Sequence[ActionProtocol]"
|
|
48
|
-
donedata: "ActionProtocol | None"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class StateKwargs(BaseStateKwargs, total=False):
|
|
52
|
-
states: List[State]
|
|
53
|
-
history: List[HistoryState]
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class HistoryKwargs(TypedDict, total=False):
|
|
57
|
-
name: str
|
|
58
|
-
value: Any
|
|
59
|
-
type: str
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class HistoryDefinition(HistoryKwargs, total=False):
|
|
63
|
-
on: TransitionsDict
|
|
64
|
-
transitions: TransitionsList
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class StateDefinition(BaseStateKwargs, total=False):
|
|
68
|
-
states: Dict[str, "StateDefinition"]
|
|
69
|
-
history: Dict[str, "HistoryDefinition"]
|
|
70
|
-
on: TransitionsDict
|
|
71
|
-
transitions: TransitionsList
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _parse_history(
|
|
75
|
-
states: Mapping[str, "HistoryKwargs |HistoryDefinition"],
|
|
76
|
-
) -> Tuple[Dict[str, HistoryState], Dict[str, dict]]:
|
|
77
|
-
states_instances: Dict[str, HistoryState] = {}
|
|
78
|
-
events_definitions: Dict[str, dict] = {}
|
|
79
|
-
for state_id, state_definition in states.items():
|
|
80
|
-
state_definition = cast(HistoryDefinition, state_definition)
|
|
81
|
-
transition_defs = state_definition.pop("on", {})
|
|
82
|
-
transition_list = state_definition.pop("transitions", [])
|
|
83
|
-
if transition_list:
|
|
84
|
-
transition_defs[None] = transition_list
|
|
85
|
-
|
|
86
|
-
if transition_defs:
|
|
87
|
-
events_definitions[state_id] = transition_defs
|
|
88
|
-
|
|
89
|
-
state_definition = cast(HistoryKwargs, state_definition)
|
|
90
|
-
states_instances[state_id] = HistoryState(**state_definition)
|
|
91
|
-
|
|
92
|
-
return (states_instances, events_definitions)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _parse_states(
|
|
96
|
-
states: Mapping[str, "BaseStateKwargs | StateDefinition"],
|
|
97
|
-
) -> Tuple[Dict[str, State], Dict[str, dict]]:
|
|
98
|
-
states_instances: Dict[str, State] = {}
|
|
99
|
-
events_definitions: Dict[str, dict] = {}
|
|
100
|
-
|
|
101
|
-
for state_id, state_definition in states.items():
|
|
102
|
-
# Process nested states. Replaces `states` as a definition by a list of `State` instances.
|
|
103
|
-
state_definition = cast(StateDefinition, state_definition)
|
|
104
|
-
|
|
105
|
-
# pop the nested states, history and transitions definitions
|
|
106
|
-
inner_states_defs: Dict[str, StateDefinition] = state_definition.pop("states", {})
|
|
107
|
-
inner_history_defs: Dict[str, HistoryDefinition] = state_definition.pop("history", {})
|
|
108
|
-
transition_defs = state_definition.pop("on", {})
|
|
109
|
-
transition_list = state_definition.pop("transitions", [])
|
|
110
|
-
if transition_list:
|
|
111
|
-
transition_defs[None] = transition_list
|
|
112
|
-
|
|
113
|
-
if inner_states_defs:
|
|
114
|
-
inner_states, inner_events = _parse_states(inner_states_defs)
|
|
115
|
-
|
|
116
|
-
top_level_states = [
|
|
117
|
-
state._set_id(state_id)
|
|
118
|
-
for state_id, state in inner_states.items()
|
|
119
|
-
if not state.parent
|
|
120
|
-
]
|
|
121
|
-
state_definition["states"] = top_level_states # type: ignore
|
|
122
|
-
states_instances.update(inner_states)
|
|
123
|
-
events_definitions.update(inner_events)
|
|
124
|
-
|
|
125
|
-
if inner_history_defs:
|
|
126
|
-
inner_history, inner_events = _parse_history(inner_history_defs)
|
|
127
|
-
|
|
128
|
-
top_level_history = [
|
|
129
|
-
state._set_id(state_id)
|
|
130
|
-
for state_id, state in inner_history.items()
|
|
131
|
-
if not state.parent
|
|
132
|
-
]
|
|
133
|
-
state_definition["history"] = top_level_history # type: ignore
|
|
134
|
-
states_instances.update(inner_history)
|
|
135
|
-
events_definitions.update(inner_events)
|
|
136
|
-
|
|
137
|
-
if transition_defs:
|
|
138
|
-
events_definitions[state_id] = transition_defs
|
|
139
|
-
|
|
140
|
-
state_definition = cast(BaseStateKwargs, state_definition)
|
|
141
|
-
states_instances[state_id] = State(**state_definition)
|
|
142
|
-
|
|
143
|
-
return (states_instances, events_definitions)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def create_machine_class_from_definition(
|
|
147
|
-
name: str, states: Mapping[str, "StateKwargs | StateDefinition"], **definition
|
|
148
|
-
) -> "type[StateChart]": # noqa: C901
|
|
149
|
-
"""Create a StateChart class dynamically from a dictionary definition.
|
|
150
|
-
|
|
151
|
-
Args:
|
|
152
|
-
name: The class name for the generated state machine.
|
|
153
|
-
states: A mapping of state IDs to state definitions. Each state definition
|
|
154
|
-
can include ``initial``, ``final``, ``parallel``, ``name``, ``value``,
|
|
155
|
-
``enter``/``exit`` callbacks, ``donedata``, nested ``states``,
|
|
156
|
-
``history``, and transitions via ``on`` (event-triggered) or
|
|
157
|
-
``transitions`` (eventless).
|
|
158
|
-
**definition: Additional keyword arguments passed to the metaclass
|
|
159
|
-
(e.g., ``validate_final_reachability=False``).
|
|
160
|
-
|
|
161
|
-
Returns:
|
|
162
|
-
A new StateChart subclass configured with the given states and transitions.
|
|
163
|
-
|
|
164
|
-
Example:
|
|
165
|
-
|
|
166
|
-
>>> machine = create_machine_class_from_definition(
|
|
167
|
-
... "TrafficLightMachine",
|
|
168
|
-
... **{
|
|
169
|
-
... "states": {
|
|
170
|
-
... "green": {"initial": True, "on": {"change": [{"target": "yellow"}]}},
|
|
171
|
-
... "yellow": {"on": {"change": [{"target": "red"}]}},
|
|
172
|
-
... "red": {"on": {"change": [{"target": "green"}]}},
|
|
173
|
-
... },
|
|
174
|
-
... }
|
|
175
|
-
... )
|
|
176
|
-
|
|
177
|
-
"""
|
|
178
|
-
states_instances, events_definitions = _parse_states(states)
|
|
179
|
-
|
|
180
|
-
events: Dict[str, TransitionList] = {}
|
|
181
|
-
for state_id, state_events in events_definitions.items():
|
|
182
|
-
for event_name, transitions_data in state_events.items():
|
|
183
|
-
for transition_data in transitions_data:
|
|
184
|
-
source = states_instances[state_id]
|
|
185
|
-
|
|
186
|
-
target_state_id = transition_data["target"]
|
|
187
|
-
transition_event_name = transition_data.get("event")
|
|
188
|
-
if event_name is not None and transition_event_name is not None:
|
|
189
|
-
transition_event_name = f"{event_name} {transition_event_name}"
|
|
190
|
-
elif event_name is not None:
|
|
191
|
-
transition_event_name = event_name
|
|
192
|
-
|
|
193
|
-
transition_kwargs = {
|
|
194
|
-
"event": transition_event_name,
|
|
195
|
-
"internal": transition_data.get("internal"),
|
|
196
|
-
"initial": transition_data.get("initial"),
|
|
197
|
-
"cond": transition_data.get("cond"),
|
|
198
|
-
"unless": transition_data.get("unless"),
|
|
199
|
-
"on": transition_data.get("on"),
|
|
200
|
-
"before": transition_data.get("before"),
|
|
201
|
-
"after": transition_data.get("after"),
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
# Handle multi-target transitions (space-separated target IDs)
|
|
205
|
-
if target_state_id and isinstance(target_state_id, str) and " " in target_state_id:
|
|
206
|
-
target_ids = target_state_id.split()
|
|
207
|
-
targets = [states_instances[tid] for tid in target_ids]
|
|
208
|
-
t = Transition(source, target=targets, **transition_kwargs)
|
|
209
|
-
source.transitions.add_transitions(t)
|
|
210
|
-
transition = TransitionList([t])
|
|
211
|
-
else:
|
|
212
|
-
target = states_instances[target_state_id] if target_state_id else None
|
|
213
|
-
transition = source.to(target, **transition_kwargs)
|
|
214
|
-
|
|
215
|
-
if event_name in events:
|
|
216
|
-
events[event_name] |= transition
|
|
217
|
-
elif event_name is not None:
|
|
218
|
-
events[event_name] = transition
|
|
219
|
-
|
|
220
|
-
top_level_states = {
|
|
221
|
-
state_id: state for state_id, state in states_instances.items() if not state.parent
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
attrs_mapper = {**definition, **top_level_states, **events}
|
|
225
|
-
return StateMachineMetaclass(name, (StateChart,), attrs_mapper) # type: ignore[return-value]
|
|
1
|
+
"""Load a statechart from a declarative document (SCXML, JSON or YAML).
|
|
2
|
+
|
|
3
|
+
This package is the high-level facade for building a running state machine from a document
|
|
4
|
+
instead of a Python class. :func:`~statemachine.io.load` is the entry point: it detects the
|
|
5
|
+
format (by file extension, or via an explicit ``format=``), parses the text into a neutral
|
|
6
|
+
intermediate representation, compiles it with a secure-by-default evaluator and returns a
|
|
7
|
+
ready-to-instantiate :class:`~statemachine.statemachine.StateChart` class. Every format runs
|
|
8
|
+
under the same execution model, so a guard, an action or a nested machine behaves identically
|
|
9
|
+
whether it was authored in SCXML, JSON or YAML.
|
|
10
|
+
|
|
11
|
+
Security
|
|
12
|
+
--------
|
|
13
|
+
|
|
14
|
+
Loading a document compiles its guards, datamodel expressions and executable content into
|
|
15
|
+
callables. Because a document may come from a semi-trusted source, loading is **secure by
|
|
16
|
+
default** (``trusted=False``): expressions are evaluated by a **restricted AST-whitelist
|
|
17
|
+
evaluator** that cannot reach builtins, dunder attributes, or arbitrary calls, and
|
|
18
|
+
``<script>`` / ``script`` (arbitrary code) is rejected. This mirrors ``yaml.safe_load`` and
|
|
19
|
+
keeps loading from turning into arbitrary code execution.
|
|
20
|
+
|
|
21
|
+
Passing ``trusted=True`` restores full ``eval``/``exec`` evaluation and enables ``script``.
|
|
22
|
+
In that mode a document is equivalent to executable Python (much like :mod:`pickle`), so
|
|
23
|
+
**only load ``trusted`` documents from sources you control** (hand-authored documents, the
|
|
24
|
+
output of your own tooling, the W3C conformance suite).
|
|
25
|
+
|
|
26
|
+
See the GHSA-v4jc-pm6r-3vj8 advisory for details.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from .class_factory import ActionProtocol
|
|
30
|
+
from .class_factory import HistoryDefinition
|
|
31
|
+
from .class_factory import StateDefinition
|
|
32
|
+
from .class_factory import TransitionDict
|
|
33
|
+
from .class_factory import create_machine_class_from_definition
|
|
34
|
+
from .loader import build_processor
|
|
35
|
+
from .loader import load
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"ActionProtocol",
|
|
39
|
+
"TransitionDict",
|
|
40
|
+
"StateDefinition",
|
|
41
|
+
"HistoryDefinition",
|
|
42
|
+
"create_machine_class_from_definition",
|
|
43
|
+
"load",
|
|
44
|
+
"build_processor",
|
|
45
|
+
]
|