Module pypowerautomate.actions.actions
Expand source code
import json
from typing import List, Dict, Union
from copy import deepcopy
from .base import BaseAction, SkeltonNode
from .variable import InitVariableAction
class Actions:
"""
A class that aggregates action nodes into a tree structure, corresponding to the 'Actions' field in a JSON schema.
This class defines a tree structure with the following constraints:
- The root node is always a SkeletonNode and is not included in exports.
- All nodes, except for the root, must have exactly one parent node.
- Nodes can have zero or more child nodes.
Note:
To manage variable initialization actions at the top of the chain in flows triggered by events, such actions are only allowed at the root level and must be executed in sequence before other actions.
"""
def __init__(self, is_root: bool = False) -> None:
self.root_node = SkeltonNode("root")
self.last_update_node = self.root_node
self.is_root_actions: bool = is_root
self.nodes: Dict[str, BaseAction] = {"root": self.root_node}
self.variable_init_nodes: List[BaseAction] = []
def __validate_action(self, new_action: BaseAction, prev_action: BaseAction = None):
"""
Validates a new action before adding it to the tree, ensuring it does not already exist, and checks parent-child constraints.
Args:
new_action (BaseAction): The action to be validated.
prev_action (BaseAction): The previous action in the tree to which the new action would be linked.
Raises:
ValueError: If the action violates any constraints.
"""
if new_action in self.nodes.values():
raise ValueError(f"{new_action} already exists in Actions")
if new_action.have_parent_node:
raise ValueError(f"{new_action} already have a parent Actions.")
if prev_action and prev_action not in self.nodes.values():
raise ValueError(f"{prev_action} not in Actions")
if not self.is_root_actions and isinstance(new_action, InitVariableAction):
raise ValueError(f"{new_action} cannot be set into non-root Actions")
original_name = new_action.action_name
counter = 1
while new_action.action_name in self.nodes:
new_action.action_name = f"{original_name}_{counter}"
counter += 1
def add_top(self, new_action: BaseAction):
"""
Adds a new action at the top of the tree under the root node.
Args:
new_action (BaseAction): The action to be added.
"""
self.__validate_action(new_action)
self.root_node.add_next_action(new_action)
new_action.have_parent_node = True
new_action.update_runafter(self.root_node)
self.last_update_node = new_action
self.nodes[new_action.action_name] = new_action
def add_after(self, new_action: BaseAction, prev_action: BaseAction, force_exec: bool = False, exec_if_failed: bool = False):
"""
Adds a new action immediately after a specified action in the tree.
Args:
new_action (BaseAction): The action to be added.
prev_action (BaseAction): The action after which the new action should be added.
force_exec (bool): If true, the new action will execute even if the previous action failed.
exec_if_failed (bool): If true, the new action will execute only if the previous action failed.
"""
self.__validate_action(new_action, prev_action)
prev_action.add_next_action(new_action)
new_action.have_parent_node = True
new_action.update_runafter(prev_action, force_exec=force_exec, exec_if_failed=exec_if_failed)
self.last_update_node = new_action
self.nodes[new_action.action_name] = new_action
def append(self, new_action: BaseAction, force_exec: bool = False, exec_if_failed: bool = False):
"""
Appends a new action to the last updated node in the tree.
Args:
new_action (BaseAction): The action to be added.
force_exec (bool): If true, the new action will execute regardless of the previous action's success.
exec_if_failed (bool): If true, the new action will execute only if the previous action failed.
"""
self.__validate_action(new_action)
self.last_update_node.add_next_action(new_action)
new_action.have_parent_node = True
new_action.update_runafter(self.last_update_node, force_exec=force_exec, exec_if_failed=exec_if_failed)
self.last_update_node = new_action
self.nodes[new_action.action_name] = new_action
def copy_nodes(self, original_node: BaseAction) -> BaseAction:
"""
Recursively copies a node and all its children.
Args:
original_node (BaseAction): The root node from which the copy will begin.
Returns:
BaseAction: The root of the copied subtree.
"""
new_node = original_node.clone()
for child in original_node.next_nodes:
new_child = self.copy_nodes(child)
new_node.add_next_action(new_child)
return new_node
def __deepcopy__(self, memo) -> 'Actions':
"""
Creates a deep copy of this Actions instance, including all nodes and their connections.
Returns:
Actions: A new Actions instance that is a deep copy of this instance.
"""
new_root = self.copy_nodes(self.root_node)
new_actions = Actions(self.is_root_actions)
new_actions.root_node = new_root
new_actions.nodes = {"root": new_root}
stack = [(new_root, None)]
while stack:
child, parent = stack.pop()
if parent:
new_actions.add_after(child, parent)
for next_node in child.next_nodes:
stack.append((next_node, child))
new_actions.last_update_node = new_actions.nodes[self.last_update_node.action_name]
return new_actions
def clone(self) -> 'Actions':
"""
Creates a clone of this Actions instance using deep copy.
Returns:
Actions: A cloned instance of this Actions.
"""
return deepcopy(self)
def __add__(self, rhs_actions: Union['Actions', BaseAction]) -> 'Actions':
"""
Supports the addition of another Actions instance or a BaseAction to this instance, combining their nodes appropriately.
Args:
rhs_actions (Union['Actions', BaseAction]): The right-hand side Actions instance or BaseAction to add.
Returns:
Actions: A new Actions instance resulting from the addition.
Raises:
TypeError: If the right-hand side is neither an Actions instance nor a BaseAction.
"""
if isinstance(rhs_actions, Actions):
new_actions = self.clone()
new_actions.is_root_actions |= rhs_actions.is_root_actions
stack = [(rhs_actions.root_node, None)]
while stack:
child, parent = stack.pop()
new_child = child.clone()
for next_node in child.next_nodes:
if isinstance(new_child, SkeltonNode):
stack.append((next_node, new_actions.last_update_node))
else:
stack.append((next_node, new_child))
if not isinstance(new_child, SkeltonNode):
new_actions.add_after(new_child, parent)
return new_actions
elif isinstance(rhs_actions, BaseAction):
new_actions = self.clone()
new_actions.append(rhs_actions.clone())
return new_actions
raise TypeError("Both operand must be instance of the Actions class or BaseAction")
def export(self) -> Dict:
"""
Exports the Actions tree to a dictionary, excluding the root node.
Returns:
Dict: A dictionary representation of all actions except the root.
"""
d = {}
for node in self.nodes.values():
if isinstance(node, SkeltonNode):
continue
d[node.action_name] = node.export()
return d
class RawActions:
"""
A class that behaves like the Actions class but takes exported data as input and re-validates it.
Args:
definition (Dict): A dictionary representation of actions.
"""
def __init__(self, definition: Dict):
self.definition: Dict = definition
def validation(self) -> bool:
"""
Validates the structure of the actions dictionary, ensuring each action has required properties and there are no duplicate names or unresolved dependencies.
Returns:
bool: True if the dictionary is valid, False otherwise.
"""
if not self.definition:
return False
used_names = set()
for action_name, node in self.definition.items():
if action_name in used_names:
return False
used_names.add(action_name)
if not isinstance(node, dict) or {"type", "inputs", "metadata"} - node.keys():
return False
if "runAfter" in node and any(name not in used_names for name in node["runAfter"]):
return False
return True
def export(self) -> Dict:
"""
Re-exports the actions dictionary if it passes validation, otherwise returns an empty dictionary.
Returns:
Dict: The validated actions dictionary, or an empty dictionary if validation fails.
"""
return self.definition if self.validation() else {}
Classes
class Actions (is_root: bool = False)
-
A class that aggregates action nodes into a tree structure, corresponding to the 'Actions' field in a JSON schema.
This class defines a tree structure with the following constraints: - The root node is always a SkeletonNode and is not included in exports. - All nodes, except for the root, must have exactly one parent node. - Nodes can have zero or more child nodes.
Note: To manage variable initialization actions at the top of the chain in flows triggered by events, such actions are only allowed at the root level and must be executed in sequence before other actions.
Expand source code
class Actions: """ A class that aggregates action nodes into a tree structure, corresponding to the 'Actions' field in a JSON schema. This class defines a tree structure with the following constraints: - The root node is always a SkeletonNode and is not included in exports. - All nodes, except for the root, must have exactly one parent node. - Nodes can have zero or more child nodes. Note: To manage variable initialization actions at the top of the chain in flows triggered by events, such actions are only allowed at the root level and must be executed in sequence before other actions. """ def __init__(self, is_root: bool = False) -> None: self.root_node = SkeltonNode("root") self.last_update_node = self.root_node self.is_root_actions: bool = is_root self.nodes: Dict[str, BaseAction] = {"root": self.root_node} self.variable_init_nodes: List[BaseAction] = [] def __validate_action(self, new_action: BaseAction, prev_action: BaseAction = None): """ Validates a new action before adding it to the tree, ensuring it does not already exist, and checks parent-child constraints. Args: new_action (BaseAction): The action to be validated. prev_action (BaseAction): The previous action in the tree to which the new action would be linked. Raises: ValueError: If the action violates any constraints. """ if new_action in self.nodes.values(): raise ValueError(f"{new_action} already exists in Actions") if new_action.have_parent_node: raise ValueError(f"{new_action} already have a parent Actions.") if prev_action and prev_action not in self.nodes.values(): raise ValueError(f"{prev_action} not in Actions") if not self.is_root_actions and isinstance(new_action, InitVariableAction): raise ValueError(f"{new_action} cannot be set into non-root Actions") original_name = new_action.action_name counter = 1 while new_action.action_name in self.nodes: new_action.action_name = f"{original_name}_{counter}" counter += 1 def add_top(self, new_action: BaseAction): """ Adds a new action at the top of the tree under the root node. Args: new_action (BaseAction): The action to be added. """ self.__validate_action(new_action) self.root_node.add_next_action(new_action) new_action.have_parent_node = True new_action.update_runafter(self.root_node) self.last_update_node = new_action self.nodes[new_action.action_name] = new_action def add_after(self, new_action: BaseAction, prev_action: BaseAction, force_exec: bool = False, exec_if_failed: bool = False): """ Adds a new action immediately after a specified action in the tree. Args: new_action (BaseAction): The action to be added. prev_action (BaseAction): The action after which the new action should be added. force_exec (bool): If true, the new action will execute even if the previous action failed. exec_if_failed (bool): If true, the new action will execute only if the previous action failed. """ self.__validate_action(new_action, prev_action) prev_action.add_next_action(new_action) new_action.have_parent_node = True new_action.update_runafter(prev_action, force_exec=force_exec, exec_if_failed=exec_if_failed) self.last_update_node = new_action self.nodes[new_action.action_name] = new_action def append(self, new_action: BaseAction, force_exec: bool = False, exec_if_failed: bool = False): """ Appends a new action to the last updated node in the tree. Args: new_action (BaseAction): The action to be added. force_exec (bool): If true, the new action will execute regardless of the previous action's success. exec_if_failed (bool): If true, the new action will execute only if the previous action failed. """ self.__validate_action(new_action) self.last_update_node.add_next_action(new_action) new_action.have_parent_node = True new_action.update_runafter(self.last_update_node, force_exec=force_exec, exec_if_failed=exec_if_failed) self.last_update_node = new_action self.nodes[new_action.action_name] = new_action def copy_nodes(self, original_node: BaseAction) -> BaseAction: """ Recursively copies a node and all its children. Args: original_node (BaseAction): The root node from which the copy will begin. Returns: BaseAction: The root of the copied subtree. """ new_node = original_node.clone() for child in original_node.next_nodes: new_child = self.copy_nodes(child) new_node.add_next_action(new_child) return new_node def __deepcopy__(self, memo) -> 'Actions': """ Creates a deep copy of this Actions instance, including all nodes and their connections. Returns: Actions: A new Actions instance that is a deep copy of this instance. """ new_root = self.copy_nodes(self.root_node) new_actions = Actions(self.is_root_actions) new_actions.root_node = new_root new_actions.nodes = {"root": new_root} stack = [(new_root, None)] while stack: child, parent = stack.pop() if parent: new_actions.add_after(child, parent) for next_node in child.next_nodes: stack.append((next_node, child)) new_actions.last_update_node = new_actions.nodes[self.last_update_node.action_name] return new_actions def clone(self) -> 'Actions': """ Creates a clone of this Actions instance using deep copy. Returns: Actions: A cloned instance of this Actions. """ return deepcopy(self) def __add__(self, rhs_actions: Union['Actions', BaseAction]) -> 'Actions': """ Supports the addition of another Actions instance or a BaseAction to this instance, combining their nodes appropriately. Args: rhs_actions (Union['Actions', BaseAction]): The right-hand side Actions instance or BaseAction to add. Returns: Actions: A new Actions instance resulting from the addition. Raises: TypeError: If the right-hand side is neither an Actions instance nor a BaseAction. """ if isinstance(rhs_actions, Actions): new_actions = self.clone() new_actions.is_root_actions |= rhs_actions.is_root_actions stack = [(rhs_actions.root_node, None)] while stack: child, parent = stack.pop() new_child = child.clone() for next_node in child.next_nodes: if isinstance(new_child, SkeltonNode): stack.append((next_node, new_actions.last_update_node)) else: stack.append((next_node, new_child)) if not isinstance(new_child, SkeltonNode): new_actions.add_after(new_child, parent) return new_actions elif isinstance(rhs_actions, BaseAction): new_actions = self.clone() new_actions.append(rhs_actions.clone()) return new_actions raise TypeError("Both operand must be instance of the Actions class or BaseAction") def export(self) -> Dict: """ Exports the Actions tree to a dictionary, excluding the root node. Returns: Dict: A dictionary representation of all actions except the root. """ d = {} for node in self.nodes.values(): if isinstance(node, SkeltonNode): continue d[node.action_name] = node.export() return d
Methods
def add_after(self, new_action: BaseAction, prev_action: BaseAction, force_exec: bool = False, exec_if_failed: bool = False)
-
Adds a new action immediately after a specified action in the tree.
Args
new_action
:BaseAction
- The action to be added.
prev_action
:BaseAction
- The action after which the new action should be added.
force_exec
:bool
- If true, the new action will execute even if the previous action failed.
exec_if_failed
:bool
- If true, the new action will execute only if the previous action failed.
Expand source code
def add_after(self, new_action: BaseAction, prev_action: BaseAction, force_exec: bool = False, exec_if_failed: bool = False): """ Adds a new action immediately after a specified action in the tree. Args: new_action (BaseAction): The action to be added. prev_action (BaseAction): The action after which the new action should be added. force_exec (bool): If true, the new action will execute even if the previous action failed. exec_if_failed (bool): If true, the new action will execute only if the previous action failed. """ self.__validate_action(new_action, prev_action) prev_action.add_next_action(new_action) new_action.have_parent_node = True new_action.update_runafter(prev_action, force_exec=force_exec, exec_if_failed=exec_if_failed) self.last_update_node = new_action self.nodes[new_action.action_name] = new_action
def add_top(self, new_action: BaseAction)
-
Adds a new action at the top of the tree under the root node.
Args
new_action
:BaseAction
- The action to be added.
Expand source code
def add_top(self, new_action: BaseAction): """ Adds a new action at the top of the tree under the root node. Args: new_action (BaseAction): The action to be added. """ self.__validate_action(new_action) self.root_node.add_next_action(new_action) new_action.have_parent_node = True new_action.update_runafter(self.root_node) self.last_update_node = new_action self.nodes[new_action.action_name] = new_action
def append(self, new_action: BaseAction, force_exec: bool = False, exec_if_failed: bool = False)
-
Appends a new action to the last updated node in the tree.
Args
new_action
:BaseAction
- The action to be added.
force_exec
:bool
- If true, the new action will execute regardless of the previous action's success.
exec_if_failed
:bool
- If true, the new action will execute only if the previous action failed.
Expand source code
def append(self, new_action: BaseAction, force_exec: bool = False, exec_if_failed: bool = False): """ Appends a new action to the last updated node in the tree. Args: new_action (BaseAction): The action to be added. force_exec (bool): If true, the new action will execute regardless of the previous action's success. exec_if_failed (bool): If true, the new action will execute only if the previous action failed. """ self.__validate_action(new_action) self.last_update_node.add_next_action(new_action) new_action.have_parent_node = True new_action.update_runafter(self.last_update_node, force_exec=force_exec, exec_if_failed=exec_if_failed) self.last_update_node = new_action self.nodes[new_action.action_name] = new_action
def clone(self) ‑> Actions
-
Creates a clone of this Actions instance using deep copy.
Returns
Actions
- A cloned instance of this Actions.
Expand source code
def clone(self) -> 'Actions': """ Creates a clone of this Actions instance using deep copy. Returns: Actions: A cloned instance of this Actions. """ return deepcopy(self)
def copy_nodes(self, original_node: BaseAction) ‑> BaseAction
-
Recursively copies a node and all its children.
Args
original_node
:BaseAction
- The root node from which the copy will begin.
Returns
BaseAction
- The root of the copied subtree.
Expand source code
def copy_nodes(self, original_node: BaseAction) -> BaseAction: """ Recursively copies a node and all its children. Args: original_node (BaseAction): The root node from which the copy will begin. Returns: BaseAction: The root of the copied subtree. """ new_node = original_node.clone() for child in original_node.next_nodes: new_child = self.copy_nodes(child) new_node.add_next_action(new_child) return new_node
def export(self) ‑> Dict
-
Exports the Actions tree to a dictionary, excluding the root node.
Returns
Dict
- A dictionary representation of all actions except the root.
Expand source code
def export(self) -> Dict: """ Exports the Actions tree to a dictionary, excluding the root node. Returns: Dict: A dictionary representation of all actions except the root. """ d = {} for node in self.nodes.values(): if isinstance(node, SkeltonNode): continue d[node.action_name] = node.export() return d
class RawActions (definition: Dict)
-
A class that behaves like the Actions class but takes exported data as input and re-validates it.
Args
definition
:Dict
- A dictionary representation of actions.
Expand source code
class RawActions: """ A class that behaves like the Actions class but takes exported data as input and re-validates it. Args: definition (Dict): A dictionary representation of actions. """ def __init__(self, definition: Dict): self.definition: Dict = definition def validation(self) -> bool: """ Validates the structure of the actions dictionary, ensuring each action has required properties and there are no duplicate names or unresolved dependencies. Returns: bool: True if the dictionary is valid, False otherwise. """ if not self.definition: return False used_names = set() for action_name, node in self.definition.items(): if action_name in used_names: return False used_names.add(action_name) if not isinstance(node, dict) or {"type", "inputs", "metadata"} - node.keys(): return False if "runAfter" in node and any(name not in used_names for name in node["runAfter"]): return False return True def export(self) -> Dict: """ Re-exports the actions dictionary if it passes validation, otherwise returns an empty dictionary. Returns: Dict: The validated actions dictionary, or an empty dictionary if validation fails. """ return self.definition if self.validation() else {}
Methods
def export(self) ‑> Dict
-
Re-exports the actions dictionary if it passes validation, otherwise returns an empty dictionary.
Returns
Dict
- The validated actions dictionary, or an empty dictionary if validation fails.
Expand source code
def export(self) -> Dict: """ Re-exports the actions dictionary if it passes validation, otherwise returns an empty dictionary. Returns: Dict: The validated actions dictionary, or an empty dictionary if validation fails. """ return self.definition if self.validation() else {}
def validation(self) ‑> bool
-
Validates the structure of the actions dictionary, ensuring each action has required properties and there are no duplicate names or unresolved dependencies.
Returns
bool
- True if the dictionary is valid, False otherwise.
Expand source code
def validation(self) -> bool: """ Validates the structure of the actions dictionary, ensuring each action has required properties and there are no duplicate names or unresolved dependencies. Returns: bool: True if the dictionary is valid, False otherwise. """ if not self.definition: return False used_names = set() for action_name, node in self.definition.items(): if action_name in used_names: return False used_names.add(action_name) if not isinstance(node, dict) or {"type", "inputs", "metadata"} - node.keys(): return False if "runAfter" in node and any(name not in used_names for name in node["runAfter"]): return False return True