From aac4e37e9022b54763f06307236adfcefc630aad Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Fri, 4 Jul 2025 20:31:16 +0100 Subject: [PATCH 1/3] added tags to machine states with information on the type of state/action Signed-off-by: Pedro Escaleira --- .../sdk/state_machine_extensions.py | 41 +++++ .../sdk/state_machine_generator.py | 164 +++++++++++++----- .../sdk/state_machine_helper.py | 91 +++++----- 3 files changed, 214 insertions(+), 82 deletions(-) create mode 100644 serverlessworkflow/sdk/state_machine_extensions.py diff --git a/serverlessworkflow/sdk/state_machine_extensions.py b/serverlessworkflow/sdk/state_machine_extensions.py new file mode 100644 index 0000000..4b22b7b --- /dev/null +++ b/serverlessworkflow/sdk/state_machine_extensions.py @@ -0,0 +1,41 @@ +from transitions.extensions.states import add_state_features, Tags, State +from transitions.extensions import ( + HierarchicalMachine, + GraphMachine, + HierarchicalGraphMachine, +) + + +class Metadata(State): + """Allows states to have metadata. + Attributes: + metadata (dict): A dictionary with the state metadata. + """ + + def __init__(self, *args, **kwargs): + """ + Args: + **kwargs: If kwargs contains `metadata`, assign them to the attribute. + """ + self.metadata = kwargs.pop("metadata", []) + super(Metadata, self).__init__(*args, **kwargs) + + def __getattr__(self, key): + if value := self.metadata.get(key) is not None: + return value + return super(Metadata, self).__getattribute__(key) + + +@add_state_features(Tags, Metadata) +class CustomHierarchicalMachine(HierarchicalMachine): + pass + + +@add_state_features(Tags, Metadata) +class CustomHierarchicalGraphMachine(HierarchicalGraphMachine): + pass + + +@add_state_features(Tags, Metadata) +class CustomGraphMachine(GraphMachine): + pass diff --git a/serverlessworkflow/sdk/state_machine_generator.py b/serverlessworkflow/sdk/state_machine_generator.py index d5aa914..84279f8 100644 --- a/serverlessworkflow/sdk/state_machine_generator.py +++ b/serverlessworkflow/sdk/state_machine_generator.py @@ -1,12 +1,19 @@ from typing import Any, Dict, List, Optional, Union from serverlessworkflow.sdk.action import Action -from serverlessworkflow.sdk.callback_state import CallbackState from serverlessworkflow.sdk.function_ref import FunctionRef -from serverlessworkflow.sdk.sleep_state import SleepState +from serverlessworkflow.sdk.state_machine_extensions import ( + CustomGraphMachine, + CustomHierarchicalGraphMachine, + CustomHierarchicalMachine, +) from serverlessworkflow.sdk.transition import Transition from serverlessworkflow.sdk.workflow import ( State, + EventState, + SleepState, + CallbackState, DataBasedSwitchState, + InjectState, EventBasedSwitchState, ParallelState, OperationState, @@ -27,7 +34,7 @@ class StateMachineGenerator: def __init__( self, state: State, - state_machine: Union[HierarchicalMachine, GraphMachine], + state_machine: Union[CustomHierarchicalMachine, CustomGraphMachine], subflows: List[Workflow] = [], is_first_state=False, get_actions=False, @@ -38,13 +45,20 @@ def __init__( self.get_actions = get_actions self.subflows = subflows - if self.get_actions and not isinstance(self.state_machine, HierarchicalMachine): + if ( + self.get_actions + and not isinstance(self.state_machine, CustomHierarchicalMachine) + and not isinstance(self.state_machine, CustomHierarchicalGraphMachine) + ): raise AttributeError( - "The provided state machine must be of the HierarchicalMachine type." + "The provided state machine must be of the CustomHierarchicalMachine or CustomHierarchicalGraphMachine types." ) - if not self.get_actions and isinstance(self.state_machine, HierarchicalMachine): + if not self.get_actions and ( + isinstance(self.state_machine, CustomHierarchicalMachine) + or isinstance(self.state_machine, CustomHierarchicalGraphMachine) + ): raise AttributeError( - "The provided state machine can not be of the HierarchicalMachine type." + "The provided state machine can not be of the CustomHierarchicalMachine or CustomHierarchicalGraphMachine types." ) def generate(self): @@ -65,12 +79,7 @@ def transitions(self): def start_transition(self): if self.is_first_state: - state_name = self.state.name - if state_name not in self.state_machine.states.keys(): - self.state_machine.add_states(state_name) - self.state_machine._initial = state_name - else: - self.state_machine._initial = state_name + self.state_machine._initial = self.state.name def data_conditions_transitions(self): if isinstance(self.state, DataBasedSwitchState): @@ -153,7 +162,7 @@ def definitions(self): if state_type == "sleep": self.sleep_state_details() elif state_type == "event": - pass + self.event_state_details() elif state_type == "operation": self.operation_state_details() elif state_type == "parallel": @@ -166,7 +175,7 @@ def definitions(self): else: raise Exception(f"Unexpected switch type;\n state value= {self.state}") elif state_type == "inject": - pass + self.inject_state_details() elif state_type == "foreach": self.foreach_state_details() elif state_type == "callback": @@ -178,10 +187,10 @@ def definitions(self): def parallel_state_details(self): if isinstance(self.state, ParallelState): - if self.state.name not in self.state_machine.states.keys(): - self.state_machine.add_states(self.state.name) - if self.is_first_state: - self.state_machine._initial = self.state.name + state_name = self.state.name + if state_name not in self.state_machine.states.keys(): + self.state_machine.add_states(state_name) + self.state_machine.get_state(state_name).tags = ["parallel_state"] state_name = self.state.name branches = self.state.branches @@ -192,42 +201,82 @@ def parallel_state_details(self): if hasattr(branch, "actions") and branch.actions: branch_name = branch.name self.state_machine.get_state(state_name).add_substates( - NestedState(branch_name) + branch_state := self.state_machine.state_cls( + branch_name + ) ) self.state_machine.get_state(state_name).initial.append( branch_name ) - branch_state = self.state_machine.get_state( - state_name - ).states[branch.name] + branch_state.tags = ["branch"] self.generate_actions_info( machine_state=branch_state, state_name=f"{state_name}.{branch_name}", actions=branch.actions, ) - def event_based_switch_state_details(self): ... + def event_based_switch_state_details(self): + if isinstance(self.state, EventBasedSwitchState): + state_name = self.state.name + if state_name not in self.state_machine.states.keys(): + self.state_machine.add_states(state_name) + self.state_machine.get_state(state_name).tags = [ + "event_based_switch_state", + "switch_state", + ] - def data_based_switch_state_details(self): ... + def data_based_switch_state_details(self): + if isinstance(self.state, DataBasedSwitchState): + state_name = self.state.name + if state_name not in self.state_machine.states.keys(): + self.state_machine.add_states(state_name) + self.state_machine.get_state(state_name).tags = [ + "data_based_switch_state", + "switch_state", + ] - def operation_state_details(self): - if self.state.name not in self.state_machine.states.keys(): - self.state_machine.add_states(self.state.name) - if self.is_first_state: - self.state_machine._initial = self.state.name + def inject_state_details(self): + if isinstance(self.state, InjectState): + state_name = self.state.name + if state_name not in self.state_machine.states.keys(): + self.state_machine.add_states(state_name) + self.state_machine.get_state(state_name).tags = ["inject_state"] + def operation_state_details(self): if isinstance(self.state, OperationState): + state_name = self.state.name + if state_name not in self.state_machine.states.keys(): + self.state_machine.add_states(state_name) + (machine_state := self.state_machine.get_state(state_name)).tags = [ + "operation_state" + ] self.generate_actions_info( - machine_state=self.state_machine.get_state(self.state.name), + machine_state=machine_state, state_name=self.state.name, actions=self.state.actions, action_mode=self.state.actionMode, ) - def sleep_state_details(self): ... + def sleep_state_details(self): + if isinstance(self.state, SleepState): + state_name = self.state.name + if state_name not in self.state_machine.states.keys(): + self.state_machine.add_states(state_name) + self.state_machine.get_state(state_name).tags = ["sleep_state"] + + def event_state_details(self): + if isinstance(self.state, EventState): + state_name = self.state.name + if state_name not in self.state_machine.states.keys(): + self.state_machine.add_states(state_name) + self.state_machine.get_state(state_name).tags = ["event_state"] def foreach_state_details(self): if isinstance(self.state, ForEachState): + state_name = self.state.name + if state_name not in self.state_machine.states.keys(): + self.state_machine.add_states(state_name) + self.state_machine.get_state(state_name).tags = ["foreach_state"] self.generate_actions_info( machine_state=self.state_machine.get_state(self.state.name), state_name=self.state.name, @@ -237,6 +286,10 @@ def foreach_state_details(self): def callback_state_details(self): if isinstance(self.state, CallbackState): + state_name = self.state.name + if state_name not in self.state_machine.states.keys(): + self.state_machine.add_states(state_name) + self.state_machine.get_state(state_name).tags = ["callback_state"] action = self.state.action if action and action.functionRef: self.generate_actions_info( @@ -264,7 +317,7 @@ def get_subflow_state( or not workflow_version ): none_found = False - new_machine = HierarchicalMachine( + new_machine = CustomHierarchicalMachine( model=None, initial=None, auto_transitions=False ) @@ -282,7 +335,8 @@ def get_subflow_state( added_states[i] = self.subflow_state_name( action=action, subflow=sf ) - nested_state = NestedState(added_states[i]) + nested_state = self.state_machine.state_cls(added_states[i]) + nested_state.tags = ["subflow"] machine_state.add_substate(nested_state) self.state_machine_to_nested_state( state_name=state_name, @@ -301,7 +355,7 @@ def generate_actions_info( self, machine_state: NestedState, state_name: str, - actions: List[Dict[str, Any]], + actions: List[Dict[str, Action]], action_mode: str = "sequential", ): parallel_states = [] @@ -322,9 +376,19 @@ def generate_actions_info( ) ) if name not in machine_state.states.keys(): - machine_state.add_substate(NestedState(name)) + machine_state.add_substate( + ns := self.state_machine.state_cls(name) + ) + ns.tags = ["function"] elif action.subFlowRef: name = new_subflows_names.get(i) + elif action.eventRef: + name = f"{action.eventRef.triggerEventRef}/{action.eventRef.resultEventRef}" + if name not in machine_state.states.keys(): + machine_state.add_substate( + ns := self.state_machine.state_cls(name) + ) + ns.tags = ["event"] if name: if action_mode == "sequential": if i < len(actions) - 1: @@ -348,9 +412,24 @@ def generate_actions_info( state_name ).states.keys() ): - machine_state.add_substate(NestedState(next_name)) + machine_state.add_substate( + ns := self.state_machine.state_cls(next_name) + ) + ns.tags = ["function"] elif actions[i + 1].subFlowRef: next_name = new_subflows_names.get(i + 1) + elif actions[i + 1].eventRef: + next_name = f"{action.eventRef.triggerEventRef}/{action.eventRef.resultEventRef}" + if ( + next_name + not in self.state_machine.get_state( + state_name + ).states.keys() + ): + machine_state.add_substate( + ns := self.state_machine.state_cls(name) + ) + ns.tags = ["event"] self.state_machine.add_transition( trigger="", source=f"{state_name}.{name}", @@ -371,21 +450,22 @@ def subflow_state_name(self, action: Action, subflow: Workflow): ) def add_all_sub_states( - cls, - original_state: Union[NestedState, HierarchicalMachine], + self, + original_state: Union[NestedState, CustomHierarchicalMachine], new_state: NestedState, ): if len(original_state.states) == 0: return for substate in original_state.states.values(): - new_state.add_substate(ns := NestedState(substate.name)) - cls.add_all_sub_states(substate, ns) + new_state.add_substate(ns := self.state_machine.state_cls(substate.name)) + ns.tags = substate.tags + self.add_all_sub_states(substate, ns) new_state.initial = original_state.initial def state_machine_to_nested_state( self, state_name: str, - state_machine: HierarchicalMachine, + state_machine: CustomHierarchicalMachine, nested_state: NestedState, ) -> NestedState: self.add_all_sub_states(state_machine, nested_state) diff --git a/serverlessworkflow/sdk/state_machine_helper.py b/serverlessworkflow/sdk/state_machine_helper.py index c95158e..8e19ea0 100644 --- a/serverlessworkflow/sdk/state_machine_helper.py +++ b/serverlessworkflow/sdk/state_machine_helper.py @@ -2,13 +2,45 @@ from serverlessworkflow.sdk.workflow import Workflow from serverlessworkflow.sdk.state_machine_generator import StateMachineGenerator from transitions.extensions.diagrams import HierarchicalGraphMachine, GraphMachine +from serverlessworkflow.sdk.state_machine_extensions import ( + CustomGraphMachine, + CustomHierarchicalGraphMachine, +) from transitions.extensions.nesting import NestedState from transitions.extensions.diagrams_base import BaseGraph class StateMachineHelper: - FINAL_NODE_STYLE = {"fillcolor": "lightgreen", "peripheries": "2", "color": "green"} - NESTED_NODE_STYLE = {"fillcolor": "cornflowerblue"} + FINAL_NODE_STYLE = {"peripheries": "2", "color": "red"} + INITIAL_NODE_STYLE = {"peripheries": "2", "color": "green"} + TAGS = [ + "parallel_state", + "switch_state", + "inject_state", + "operation_state", + "sleep_state", + "event_state", + "foreach_state", + "callback_state", + "subflow", + "function", + "event", + "branch", + ] + COLORS = [ + "#8dd3c7", + "#ffffb3", + "#bebada", + "#fb8072", + "#80b1d3", + "#fdb462", + "#b3de69", + "#fccde5", + "#d9d9d9", + "#bc80bd", + "#ccebc5", + "#ffed6f", + ] def __init__( self, @@ -20,7 +52,9 @@ def __init__( self.subflows = subflows self.get_actions = get_actions - machine_type = HierarchicalGraphMachine if self.get_actions else GraphMachine + machine_type = ( + CustomHierarchicalGraphMachine if self.get_actions else CustomGraphMachine + ) # Generate machine self.machine = machine_type( @@ -40,10 +74,11 @@ def __init__( ).generate() delattr(self.machine, "get_graph") + del self.machine.style_attributes["node"]["active"] + del self.machine.style_attributes["graph"]["active"] self.machine.add_model(machine_type.self_literal) def draw(self, filename: str, graph_engine="pygraphviz"): - final_nested = [] if graph_engine == "mermaid": self.machine.graph_cls = self.machine._init_graphviz_engine( graph_engine="mermaid" @@ -51,13 +86,6 @@ def draw(self, filename: str, graph_engine="pygraphviz"): self.machine.model_graphs[id(self.machine.model)] = self.machine.graph_cls( self.machine ) - self.machine.model_graphs[id(self.machine.model)].set_node_style( - getattr(self.machine.model, self.machine.model_attribute), "active" - ) - if graph_engine != "mermaid": - if self.get_actions: - for _, s in self.machine.states.items(): - final_nested.extend(self._get_nested_active_states(s)) # Define style for name in ( @@ -65,41 +93,24 @@ def draw(self, filename: str, graph_engine="pygraphviz"): if self.get_actions else self.machine.states.keys() ): - if self.machine.get_state(name).final or name in final_nested: + if self.machine.get_state(name).final or self.machine.initial == name: self.machine.style_attributes["node"][name] = ( self.FINAL_NODE_STYLE if self.machine.get_state(name).final - else self.NESTED_NODE_STYLE + else self.INITIAL_NODE_STYLE ) self.machine.model_graphs[id(self.machine.model)].set_node_style( name, name ) - self.machine.get_graph().draw(filename, prog="dot") - - def _color_graph_nodes(self, graph: BaseGraph, final_nested: List[str] = []): - graph.graph_attr.update({"ranksep": "1.0"}) - for node in graph.nodes(): - if self.machine.get_state(str(node)).final: - graph.get_node(node).attr["fillcolor"] = "lightgreen" - graph.get_node(node).attr["peripheries"] = "2" - graph.get_node(node).attr["color"] = "green" - if str(node) in final_nested: - graph.get_node(node).attr["fillcolor"] = "cornflowerblue" + for tag in self.machine.get_state(name).tags: + if tag in self.TAGS: + self.machine.style_attributes["node"][name] = { + "fillcolor": self.COLORS[self.TAGS.index(tag)] + } + self.machine.model_graphs[id(self.machine.model)].set_node_style( + name, name + ) + break - @classmethod - def _get_nested_active_states(cls, state: NestedState, depth=0): - if len(state.states) == 0: - if depth > 0: - return [state.name] - else: - return [] - - final_states = [] - for _, nested in state.states.items(): - final_states.extend( - f"{state.name}.{n}" - for n in cls._get_nested_active_states(nested, depth + 1) - ) - - return final_states + self.machine.get_graph().draw(filename, prog="dot") From af4ea3c525891b1ae20f7807678ef0719154ef71 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Wed, 9 Jul 2025 17:56:58 +0100 Subject: [PATCH 2/3] added metadata to function and event states with the information on the action functions and events definitions Signed-off-by: Pedro Escaleira --- .../sdk/state_machine_extensions.py | 2 +- .../sdk/state_machine_generator.py | 220 +++++++++--------- .../sdk/state_machine_helper.py | 14 +- 3 files changed, 123 insertions(+), 113 deletions(-) diff --git a/serverlessworkflow/sdk/state_machine_extensions.py b/serverlessworkflow/sdk/state_machine_extensions.py index 4b22b7b..e6a0fff 100644 --- a/serverlessworkflow/sdk/state_machine_extensions.py +++ b/serverlessworkflow/sdk/state_machine_extensions.py @@ -17,7 +17,7 @@ def __init__(self, *args, **kwargs): Args: **kwargs: If kwargs contains `metadata`, assign them to the attribute. """ - self.metadata = kwargs.pop("metadata", []) + self.metadata = kwargs.pop("metadata", None) super(Metadata, self).__init__(*args, **kwargs) def __getattr__(self, key): diff --git a/serverlessworkflow/sdk/state_machine_generator.py b/serverlessworkflow/sdk/state_machine_generator.py index 84279f8..2820948 100644 --- a/serverlessworkflow/sdk/state_machine_generator.py +++ b/serverlessworkflow/sdk/state_machine_generator.py @@ -33,18 +33,19 @@ class StateMachineGenerator: def __init__( self, - state: State, + workflow: Workflow, state_machine: Union[CustomHierarchicalMachine, CustomGraphMachine], subflows: List[Workflow] = [], - is_first_state=False, get_actions=False, ): - self.state = state - self.is_first_state = is_first_state + self.workflow = workflow self.state_machine = state_machine self.get_actions = get_actions self.subflows = subflows + self.is_first_state = False + self.current_state: State = None + if ( self.get_actions and not isinstance(self.state_machine, CustomHierarchicalMachine) @@ -62,8 +63,10 @@ def __init__( ) def generate(self): - self.definitions() - self.transitions() + for self.current_state in self.workflow.states: + self.is_first_state = self.workflow.start == self.current_state.name + self.definitions() + self.transitions() def transitions(self): self.start_transition() @@ -71,21 +74,25 @@ def transitions(self): self.event_conditions_transition() self.error_transitions() self.natural_transition( - self.state.name, - self.state.transition if hasattr(self.state, "transition") else None, + self.current_state.name, + ( + self.current_state.transition + if hasattr(self.current_state, "transition") + else None + ), ) self.compensated_by_transition() self.end_transition() def start_transition(self): if self.is_first_state: - self.state_machine._initial = self.state.name + self.state_machine._initial = self.current_state.name def data_conditions_transitions(self): - if isinstance(self.state, DataBasedSwitchState): - data_conditions = self.state.dataConditions + if isinstance(self.current_state, DataBasedSwitchState): + data_conditions = self.current_state.dataConditions if data_conditions: - state_name = self.state.name + state_name = self.current_state.name for data_condition in data_conditions: if isinstance(data_condition, TransitionDataCondition): transition = data_condition.transition @@ -97,32 +104,32 @@ def data_conditions_transitions(self): ): condition = data_condition.condition self.end_state(state_name, condition=condition) - self.default_condition_transition(self.state) + self.default_condition_transition(self.current_state) def event_conditions_transition(self): - if isinstance(self.state, EventBasedSwitchState): - event_conditions = self.state.eventConditions + if isinstance(self.current_state, EventBasedSwitchState): + event_conditions = self.current_state.eventConditions if event_conditions: - state_name = self.state.name + state_name = self.current_state.name for event_condition in event_conditions: transition = event_condition.transition event_ref = event_condition.eventRef self.natural_transition(state_name, transition, event_ref) if event_condition.end: self.end_state(state_name, condition=event_ref) - self.default_condition_transition(self.state) + self.default_condition_transition(self.current_state) def default_condition_transition(self, state: State): if hasattr(state, "defaultCondition"): default_condition = state.defaultCondition if default_condition: self.natural_transition( - self.state.name, default_condition.transition, "default" + self.current_state.name, default_condition.transition, "default" ) def end_transition(self): - if hasattr(self.state, "end") and self.state.end: - self.end_state(self.state.name) + if hasattr(self.current_state, "end") and self.current_state.end: + self.end_state(self.current_state.name) def natural_transition( self, @@ -144,21 +151,25 @@ def natural_transition( ) def error_transitions(self): - if hasattr(self.state, "onErrors") and (on_errors := self.state.onErrors): + if hasattr(self.current_state, "onErrors") and ( + on_errors := self.current_state.onErrors + ): for error in on_errors: self.natural_transition( - self.state.name, + self.current_state.name, error.transition, error.errorRef, ) def compensated_by_transition(self): - compensated_by = self.state.compensatedBy + compensated_by = self.current_state.compensatedBy if compensated_by: - self.natural_transition(self.state.name, compensated_by, "compensated by") + self.natural_transition( + self.current_state.name, compensated_by, "compensated by" + ) def definitions(self): - state_type = self.state.type + state_type = self.current_state.type if state_type == "sleep": self.sleep_state_details() elif state_type == "event": @@ -168,12 +179,14 @@ def definitions(self): elif state_type == "parallel": self.parallel_state_details() elif state_type == "switch": - if self.state.dataConditions: + if self.current_state.dataConditions: self.data_based_switch_state_details() - elif self.state.eventConditions: + elif self.current_state.eventConditions: self.event_based_switch_state_details() else: - raise Exception(f"Unexpected switch type;\n state value= {self.state}") + raise Exception( + f"Unexpected switch type;\n state value= {self.current_state}" + ) elif state_type == "inject": self.inject_state_details() elif state_type == "foreach": @@ -182,18 +195,15 @@ def definitions(self): self.callback_state_details() else: raise Exception( - f"Unexpected type= {state_type};\n state value= {self.state}" + f"Unexpected type= {state_type};\n state value= {self.current_state}" ) def parallel_state_details(self): - if isinstance(self.state, ParallelState): - state_name = self.state.name - if state_name not in self.state_machine.states.keys(): - self.state_machine.add_states(state_name) - self.state_machine.get_state(state_name).tags = ["parallel_state"] - - state_name = self.state.name - branches = self.state.branches + if isinstance(self.current_state, ParallelState): + self.state_to_machine_state(["parallel_state", "state"]) + + state_name = self.current_state.name + branches = self.current_state.branches if branches: if self.get_actions: self.state_machine.get_state(state_name).initial = [] @@ -209,6 +219,9 @@ def parallel_state_details(self): branch_name ) branch_state.tags = ["branch"] + branch_state.metadata = { + "branch": self.current_state.serialize().__dict__ + } self.generate_actions_info( machine_state=branch_state, state_name=f"{state_name}.{branch_name}", @@ -216,88 +229,68 @@ def parallel_state_details(self): ) def event_based_switch_state_details(self): - if isinstance(self.state, EventBasedSwitchState): - state_name = self.state.name - if state_name not in self.state_machine.states.keys(): - self.state_machine.add_states(state_name) - self.state_machine.get_state(state_name).tags = [ - "event_based_switch_state", - "switch_state", - ] + if isinstance(self.current_state, EventBasedSwitchState): + self.state_to_machine_state( + ["event_based_switch_state", "switch_state", "state"] + ) def data_based_switch_state_details(self): - if isinstance(self.state, DataBasedSwitchState): - state_name = self.state.name - if state_name not in self.state_machine.states.keys(): - self.state_machine.add_states(state_name) - self.state_machine.get_state(state_name).tags = [ - "data_based_switch_state", - "switch_state", - ] + if isinstance(self.current_state, DataBasedSwitchState): + self.state_to_machine_state( + ["data_based_switch_state", "switch_state", "state"] + ) def inject_state_details(self): - if isinstance(self.state, InjectState): - state_name = self.state.name - if state_name not in self.state_machine.states.keys(): - self.state_machine.add_states(state_name) - self.state_machine.get_state(state_name).tags = ["inject_state"] + if isinstance(self.current_state, InjectState): + self.state_to_machine_state(["inject_state", "state"]) def operation_state_details(self): - if isinstance(self.state, OperationState): - state_name = self.state.name - if state_name not in self.state_machine.states.keys(): - self.state_machine.add_states(state_name) - (machine_state := self.state_machine.get_state(state_name)).tags = [ - "operation_state" - ] + if isinstance(self.current_state, OperationState): + machine_state = self.state_to_machine_state(["operation_state", "state"]) self.generate_actions_info( machine_state=machine_state, - state_name=self.state.name, - actions=self.state.actions, - action_mode=self.state.actionMode, + state_name=self.current_state.name, + actions=self.current_state.actions, + action_mode=self.current_state.actionMode, ) def sleep_state_details(self): - if isinstance(self.state, SleepState): - state_name = self.state.name - if state_name not in self.state_machine.states.keys(): - self.state_machine.add_states(state_name) - self.state_machine.get_state(state_name).tags = ["sleep_state"] + if isinstance(self.current_state, SleepState): + self.state_to_machine_state(["sleep_state", "state"]) def event_state_details(self): - if isinstance(self.state, EventState): - state_name = self.state.name - if state_name not in self.state_machine.states.keys(): - self.state_machine.add_states(state_name) - self.state_machine.get_state(state_name).tags = ["event_state"] + if isinstance(self.current_state, EventState): + self.state_to_machine_state(["event_state", "state"]) def foreach_state_details(self): - if isinstance(self.state, ForEachState): - state_name = self.state.name - if state_name not in self.state_machine.states.keys(): - self.state_machine.add_states(state_name) - self.state_machine.get_state(state_name).tags = ["foreach_state"] + if isinstance(self.current_state, ForEachState): + self.state_to_machine_state(["foreach_state", "state"]) self.generate_actions_info( - machine_state=self.state_machine.get_state(self.state.name), - state_name=self.state.name, - actions=self.state.actions, - action_mode=self.state.mode, + machine_state=self.state_machine.get_state(self.current_state.name), + state_name=self.current_state.name, + actions=self.current_state.actions, + action_mode=self.current_state.mode, ) def callback_state_details(self): - if isinstance(self.state, CallbackState): - state_name = self.state.name - if state_name not in self.state_machine.states.keys(): - self.state_machine.add_states(state_name) - self.state_machine.get_state(state_name).tags = ["callback_state"] - action = self.state.action + if isinstance(self.current_state, CallbackState): + self.state_to_machine_state(["callback_state", "state"]) + action = self.current_state.action if action and action.functionRef: self.generate_actions_info( - machine_state=self.state_machine.get_state(self.state.name), - state_name=self.state.name, + machine_state=self.state_machine.get_state(self.current_state.name), + state_name=self.current_state.name, actions=[action], ) + def state_to_machine_state(self, tags: List[str]) -> NestedState: + state_name = self.current_state.name + if state_name not in self.state_machine.states.keys(): + self.state_machine.add_states(state_name) + (ns := self.state_machine.get_state(state_name)).tags = tags + ns.metadata = {"state": self.current_state.serialize().__dict__} + return ns + def get_subflow_state( self, machine_state: NestedState, state_name: str, actions: List[Action] ): @@ -322,14 +315,12 @@ def get_subflow_state( ) # Generate the state machine for the subflow - for state in sf.states: - StateMachineGenerator( - state=state, - state_machine=new_machine, - is_first_state=sf.start == state.name, - get_actions=self.get_actions, - subflows=self.subflows, - ).generate() + StateMachineGenerator( + workflow=sf, + state_machine=new_machine, + get_actions=self.get_actions, + subflows=self.subflows, + ).generate() # Convert the new_machine into a NestedState added_states[i] = self.subflow_state_name( @@ -380,6 +371,7 @@ def generate_actions_info( ns := self.state_machine.state_cls(name) ) ns.tags = ["function"] + self.get_action_function(state=ns, f_name=name) elif action.subFlowRef: name = new_subflows_names.get(i) elif action.eventRef: @@ -389,6 +381,7 @@ def generate_actions_info( ns := self.state_machine.state_cls(name) ) ns.tags = ["event"] + self.get_action_event(state=ns, e_name=name) if name: if action_mode == "sequential": if i < len(actions) - 1: @@ -416,6 +409,7 @@ def generate_actions_info( ns := self.state_machine.state_cls(next_name) ) ns.tags = ["function"] + self.get_action_function(state=ns, f_name=next_name) elif actions[i + 1].subFlowRef: next_name = new_subflows_names.get(i + 1) elif actions[i + 1].eventRef: @@ -427,9 +421,10 @@ def generate_actions_info( ).states.keys() ): machine_state.add_substate( - ns := self.state_machine.state_cls(name) + ns := self.state_machine.state_cls(next_name) ) ns.tags = ["event"] + self.get_action_event(state=ns, e_name=next_name) self.state_machine.add_transition( trigger="", source=f"{state_name}.{name}", @@ -442,6 +437,22 @@ def generate_actions_info( if action_mode == "parallel": machine_state.initial = parallel_states + def get_action_function(self, state: NestedState, f_name: str): + if self.workflow.functions: + for function in self.workflow.functions: + current_function = function.serialize().__dict__ + if current_function["name"] == f_name: + state.metadata = {"function": current_function} + break + + def get_action_event(self, state: NestedState, e_name: str): + if self.workflow.events: + for event in self.workflow.events: + current_event = event.serialize().__dict__ + if current_event["name"] == e_name: + state.metadata = {"event": current_event} + break + def subflow_state_name(self, action: Action, subflow: Workflow): return ( action.name @@ -459,6 +470,7 @@ def add_all_sub_states( for substate in original_state.states.values(): new_state.add_substate(ns := self.state_machine.state_cls(substate.name)) ns.tags = substate.tags + ns.metadata = substate.metadata self.add_all_sub_states(substate, ns) new_state.initial = original_state.initial diff --git a/serverlessworkflow/sdk/state_machine_helper.py b/serverlessworkflow/sdk/state_machine_helper.py index 8e19ea0..cefeb69 100644 --- a/serverlessworkflow/sdk/state_machine_helper.py +++ b/serverlessworkflow/sdk/state_machine_helper.py @@ -64,14 +64,12 @@ def __init__( auto_transitions=False, title=title, ) - for state in workflow.states: - StateMachineGenerator( - state=state, - state_machine=self.machine, - is_first_state=workflow.start == state.name, - get_actions=self.get_actions, - subflows=subflows, - ).generate() + StateMachineGenerator( + workflow=workflow, + state_machine=self.machine, + get_actions=self.get_actions, + subflows=subflows, + ).generate() delattr(self.machine, "get_graph") del self.machine.style_attributes["node"]["active"] From f4a25611f39e9995575bf28df7bea2b081dcf529 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Thu, 10 Jul 2025 16:53:55 +0100 Subject: [PATCH 3/3] foreach state with the transition to itself Signed-off-by: Pedro Escaleira --- .../sdk/state_machine_generator.py | 182 ++++++++++-------- 1 file changed, 99 insertions(+), 83 deletions(-) diff --git a/serverlessworkflow/sdk/state_machine_generator.py b/serverlessworkflow/sdk/state_machine_generator.py index 2820948..a1e118b 100644 --- a/serverlessworkflow/sdk/state_machine_generator.py +++ b/serverlessworkflow/sdk/state_machine_generator.py @@ -265,6 +265,11 @@ def event_state_details(self): def foreach_state_details(self): if isinstance(self.current_state, ForEachState): self.state_to_machine_state(["foreach_state", "state"]) + self.state_machine.add_transition( + trigger=f"{self.current_state.iterationParam} IN {self.current_state.inputCollection}", + source=self.current_state.name, + dest=self.current_state.name, + ) self.generate_actions_info( machine_state=self.state_machine.get_state(self.current_state.name), state_name=self.current_state.name, @@ -349,93 +354,104 @@ def generate_actions_info( actions: List[Dict[str, Action]], action_mode: str = "sequential", ): - parallel_states = [] - if actions: - new_subflows_names = self.get_subflow_state( - machine_state=machine_state, state_name=state_name, actions=actions - ) - for i, action in enumerate(actions): - name = None - if action.functionRef: - name = ( - self.get_function_name(action.functionRef) - if isinstance(action.functionRef, str) - else ( - action.functionRef.refName - if isinstance(action.functionRef, FunctionRef) - else None - ) - ) - if name not in machine_state.states.keys(): - machine_state.add_substate( - ns := self.state_machine.state_cls(name) - ) - ns.tags = ["function"] - self.get_action_function(state=ns, f_name=name) - elif action.subFlowRef: - name = new_subflows_names.get(i) - elif action.eventRef: - name = f"{action.eventRef.triggerEventRef}/{action.eventRef.resultEventRef}" - if name not in machine_state.states.keys(): - machine_state.add_substate( - ns := self.state_machine.state_cls(name) + if self.get_actions: + parallel_states = [] + if actions: + new_subflows_names = self.get_subflow_state( + machine_state=machine_state, state_name=state_name, actions=actions + ) + for i, action in enumerate(actions): + name = None + if action.functionRef: + name = ( + self.get_function_name(action.functionRef) + if isinstance(action.functionRef, str) + else ( + action.functionRef.refName + if isinstance(action.functionRef, FunctionRef) + else None + ) ) - ns.tags = ["event"] - self.get_action_event(state=ns, e_name=name) - if name: - if action_mode == "sequential": - if i < len(actions) - 1: - # get next name - next_name = None - if actions[i + 1].functionRef: - next_name = ( - self.get_function_name(actions[i + 1].functionRef) - if isinstance(actions[i + 1].functionRef, str) - else ( - actions[i + 1].functionRef.refName - if isinstance( - actions[i + 1].functionRef, FunctionRef + if name not in machine_state.states.keys(): + machine_state.add_substate( + ns := self.state_machine.state_cls(name) + ) + ns.tags = ["function"] + self.get_action_function(state=ns, f_name=name) + elif action.subFlowRef: + name = new_subflows_names.get(i) + elif action.eventRef: + name = f"{action.eventRef.triggerEventRef}/{action.eventRef.resultEventRef}" + if name not in machine_state.states.keys(): + machine_state.add_substate( + ns := self.state_machine.state_cls(name) + ) + ns.tags = ["event"] + self.get_action_event(state=ns, e_name=name) + if name: + if action_mode == "sequential": + if i < len(actions) - 1: + # get next name + next_name = None + if actions[i + 1].functionRef: + next_name = ( + self.get_function_name( + actions[i + 1].functionRef + ) + if isinstance(actions[i + 1].functionRef, str) + else ( + actions[i + 1].functionRef.refName + if isinstance( + actions[i + 1].functionRef, FunctionRef + ) + else None ) - else None ) + if ( + next_name + not in self.state_machine.get_state( + state_name + ).states.keys() + ): + machine_state.add_substate( + ns := self.state_machine.state_cls( + next_name + ) + ) + ns.tags = ["function"] + self.get_action_function( + state=ns, f_name=next_name + ) + elif actions[i + 1].subFlowRef: + next_name = new_subflows_names.get(i + 1) + elif actions[i + 1].eventRef: + next_name = f"{action.eventRef.triggerEventRef}/{action.eventRef.resultEventRef}" + if ( + next_name + not in self.state_machine.get_state( + state_name + ).states.keys() + ): + machine_state.add_substate( + ns := self.state_machine.state_cls( + next_name + ) + ) + ns.tags = ["event"] + self.get_action_event( + state=ns, e_name=next_name + ) + self.state_machine.add_transition( + trigger="", + source=f"{state_name}.{name}", + dest=f"{state_name}.{next_name}", ) - if ( - next_name - not in self.state_machine.get_state( - state_name - ).states.keys() - ): - machine_state.add_substate( - ns := self.state_machine.state_cls(next_name) - ) - ns.tags = ["function"] - self.get_action_function(state=ns, f_name=next_name) - elif actions[i + 1].subFlowRef: - next_name = new_subflows_names.get(i + 1) - elif actions[i + 1].eventRef: - next_name = f"{action.eventRef.triggerEventRef}/{action.eventRef.resultEventRef}" - if ( - next_name - not in self.state_machine.get_state( - state_name - ).states.keys() - ): - machine_state.add_substate( - ns := self.state_machine.state_cls(next_name) - ) - ns.tags = ["event"] - self.get_action_event(state=ns, e_name=next_name) - self.state_machine.add_transition( - trigger="", - source=f"{state_name}.{name}", - dest=f"{state_name}.{next_name}", - ) - if i == 0: - machine_state.initial = name - elif action_mode == "parallel": - parallel_states.append(name) - if action_mode == "parallel": - machine_state.initial = parallel_states + if i == 0: + machine_state.initial = name + elif action_mode == "parallel": + parallel_states.append(name) + if action_mode == "parallel": + machine_state.initial = parallel_states def get_action_function(self, state: NestedState, f_name: str): if self.workflow.functions: