webber.edges
Helper class for edge and DAG validation logic.
1""" 2Helper class for edge and DAG validation logic. 3""" 4import typing as _T 5import uuid as _uuid 6import networkx as _nx 7import enum as _enum 8 9__all__ = ["valid_node", "valid_nodes", "valid_dag", "validate_nodes", "label_node"] 10 11class Condition(_enum.IntEnum): 12 """Represents edge condition for a node execution, based on outcome(s) of predecessor(s).""" 13 Success = 0 14 Failure = 1 15 AnyCase = 3 16 17class dotdict(dict): 18 """dot.notation access to dictionary attributes""" 19 __getattr__ = dict.get 20 __setattr__ = dict.__setitem__ 21 __delattr__ = dict.__delitem__ 22 23class edgedict(dotdict): 24 super(dotdict) 25 def __init__(self, *E, **kwargs): 26 super().__init__({'parent': E[0], 'child': E[1], 'id': E[:2]}) 27 self.update(kwargs) 28 29def continue_on_failure(edge: dict) -> bool: 30 """Check edge condition for whether to continue on parent node's failure.""" 31 return edge['Condition'] in (Condition.Failure, Condition.AnyCase) 32 33def continue_on_success(edge: dict) -> bool: 34 """Check edge condition for whether to continue on parent node's success.""" 35 return edge['Condition'] in (Condition.Success, Condition.AnyCase) 36 37def label_node(node: _T.Callable) -> str: 38 """Generates unique identifiers for Python callables in a DAG.""" 39 return f"{node.__name__}__{_uuid.uuid4()}" 40 41def get_root(graph: _nx.DiGraph) -> list: 42 """Given a network graph, return list of all nodes without incoming edges or dependencies.""" 43 return list(filter( 44 lambda node: len(list(graph.predecessors(node))) < 1, 45 graph.nodes.keys() 46 )) 47 48# TODO: Refactor logic for DAG and node validation. 49 50def valid_node(node: _T.Union[str, _T.Callable]) -> bool: 51 """Check whether given identifier represents a valid node (string or callable).""" 52 return (isinstance(node,str) or callable(node)) 53 54def valid_nodes(u_of_edge: _T.Union[str, _T.Callable], v_of_edge: _T.Union[str, _T.Callable]) -> bool: 55 """Check whether parent and child nodes represent valid nodes (string or callable).""" 56 return valid_node(u_of_edge) and valid_node(v_of_edge) 57 58def validate_nodes(u_of_edge: _T.Union[str, _T.Callable], v_of_edge: _T.Union[str, _T.Callable]) -> True: 59 """ 60 Given parent and child identifiers, validate that both represent valid nodes. 61 Otherwise raise exceptions. 62 """ 63 if not valid_node(u_of_edge): 64 err_msg = f"Outgoing node {u_of_edge} must be a string or a Python callable" 65 raise TypeError(err_msg) 66 67 if not valid_node(v_of_edge): 68 err_msg = f"Incoming node {v_of_edge} must be a string or a Python callable" 69 raise TypeError(err_msg) 70 71 return True 72 73def valid_dag(graph: _nx.Graph) -> bool: 74 """ 75 Given a network graph, return whether network is a valid DAG and that all node-keys are Python callables. 76 Meant for internal use, DAG initialization. 77 """ 78 return ( 79 isinstance(graph, _nx.Graph) and 80 _nx.is_directed_acyclic_graph(graph) and 81 not set(map(callable, list(graph.nodes.keys()))).issuperset({False}) 82 ) 83 84 85def validate_dag(graph: _nx.DiGraph) -> None: 86 """ 87 Given a network graph, validate whether graph is a valid Webber DAG. Otherwise, raise exceptions. 88 Meant for internal use, DAG initialization. 89 """ 90 if not graph.is_directed(): 91 err_msg = f"Directed graph must be defined as type {_nx.DiGraph.__name__}" 92 raise TypeError(err_msg) 93 94 if set(map(callable, list(graph.nodes.keys()))).issuperset({False}): 95 err_msg = "All registered nodes must be callable Python functions." 96 raise TypeError(err_msg) 97 98 if not _nx.is_directed_acyclic_graph(graph): 99 err_msg = "Directed acyclic graph must be properly defined --" \ 100 + "no cycles and one or more root nodes." 101 raise ValueError(err_msg)
def
valid_node(node: Union[str, Callable]) -> bool:
51def valid_node(node: _T.Union[str, _T.Callable]) -> bool: 52 """Check whether given identifier represents a valid node (string or callable).""" 53 return (isinstance(node,str) or callable(node))
Check whether given identifier represents a valid node (string or callable).
def
valid_nodes(u_of_edge: Union[str, Callable], v_of_edge: Union[str, Callable]) -> bool:
55def valid_nodes(u_of_edge: _T.Union[str, _T.Callable], v_of_edge: _T.Union[str, _T.Callable]) -> bool: 56 """Check whether parent and child nodes represent valid nodes (string or callable).""" 57 return valid_node(u_of_edge) and valid_node(v_of_edge)
Check whether parent and child nodes represent valid nodes (string or callable).
def
valid_dag(graph: networkx.classes.graph.Graph) -> bool:
74def valid_dag(graph: _nx.Graph) -> bool: 75 """ 76 Given a network graph, return whether network is a valid DAG and that all node-keys are Python callables. 77 Meant for internal use, DAG initialization. 78 """ 79 return ( 80 isinstance(graph, _nx.Graph) and 81 _nx.is_directed_acyclic_graph(graph) and 82 not set(map(callable, list(graph.nodes.keys()))).issuperset({False}) 83 )
Given a network graph, return whether network is a valid DAG and that all node-keys are Python callables. Meant for internal use, DAG initialization.
def
validate_nodes(u_of_edge: Union[str, Callable], v_of_edge: Union[str, Callable]) -> True:
59def validate_nodes(u_of_edge: _T.Union[str, _T.Callable], v_of_edge: _T.Union[str, _T.Callable]) -> True: 60 """ 61 Given parent and child identifiers, validate that both represent valid nodes. 62 Otherwise raise exceptions. 63 """ 64 if not valid_node(u_of_edge): 65 err_msg = f"Outgoing node {u_of_edge} must be a string or a Python callable" 66 raise TypeError(err_msg) 67 68 if not valid_node(v_of_edge): 69 err_msg = f"Incoming node {v_of_edge} must be a string or a Python callable" 70 raise TypeError(err_msg) 71 72 return True
Given parent and child identifiers, validate that both represent valid nodes. Otherwise raise exceptions.
def
label_node(node: Callable) -> str:
38def label_node(node: _T.Callable) -> str: 39 """Generates unique identifiers for Python callables in a DAG.""" 40 return f"{node.__name__}__{_uuid.uuid4()}"
Generates unique identifiers for Python callables in a DAG.