def add_dependency_input_connection(self, node_name, node_input_name, dependency_name, dependency_output_name): """Adds a connection from a dependency output to the input of a node :param node_name: The name of the node whose input is being connected to :type node_name: string :param node_input_name: The name of the node's input :type node_input_name: string :param dependency_name: The name of the dependency node whose output is being connected to the input :type dependency_name: string :param dependency_output_name: The name of the dependency node's output :type dependency_output_name: string :raises :class:`recipe.definition.exceptions.InvalidDefinition`: If either node is unknown, the dependency node is the wrong type, or the connection is a duplicate """ if dependency_name not in self.graph: raise InvalidDefinition( 'UNKNOWN_NODE', 'Node \'%s\' is not defined' % dependency_name) if self.graph[ dependency_name].node_type == RecipeNodeDefinition.NODE_TYPE: msg = 'Node \'%s\' cannot have a connection to a recipe node' % node_name raise InvalidDefinition('CONNECTION_INVALID_NODE', msg) connection = DependencyInputConnection(node_input_name, dependency_name, dependency_output_name) self._add_connection(node_name, connection)
def add_dependency(self, parent_name, child_name, acceptance=True): """Adds a dependency that one node has upon another node :param parent_name: The name of the parent node :type parent_name: string :param child_name: The name of the child node :type child_name: string :param acceptance: Whether the child node should run when the parent is accepted or when it is not accepted :type acceptance: bool :raises :class:`recipe.definition.exceptions.InvalidDefinition`: If either node is unknown """ if child_name not in self.graph: raise InvalidDefinition('UNKNOWN_NODE', 'Node \'%s\' is not defined' % child_name) if parent_name not in self.graph: raise InvalidDefinition('UNKNOWN_NODE', 'Node \'%s\' is not defined' % parent_name) child_node = self.graph[child_name] parent_node = self.graph[parent_name] child_node.add_dependency(parent_node, acceptance) self._topological_order = None # Invalidate cache
def __init__(self, definition=None, do_validate=False): """Creates a v1 recipe definition JSON object from the given dictionary :param definition: The recipe definition JSON dict :type definition: dict :param do_validate: Whether to perform validation on the JSON schema :type do_validate: bool :raises :class:`recipe.definition.exceptions.InvalidDdefinition`: If the given definition is invalid """ if not definition: definition = {} self._definition = definition if 'version' not in self._definition: self._definition['version'] = DEFAULT_VERSION if self._definition['version'] != DEFAULT_VERSION: msg = '%s is an unsupported version number' raise InvalidDefinition('INVALID_VERSION', msg % self._definition['version']) self._populate_default_values() try: if do_validate: validate(definition, RECIPE_DEFINITION_SCHEMA) except ValidationError as ex: raise InvalidDefinition( 'INVALID_DEFINITION', 'Invalid recipe definition: %s' % unicode(ex))
def _topological_order_visit(self, node, results, perm_set, temp_set): """Recursive depth-first search algorithm for determining a topological ordering of the recipe nodes :param node: The current node :type node: :class:`recipe.definition.node.NodeDefinition` :param results: The list of node names in topological order :type results: :func:`list` :param perm_set: A permanent set of visited nodes (node names) :type perm_set: set :param temp_set: A temporary set of visited nodes (node names) :type temp_set: set :returns: A list of nodes in topological order :rtype: :func:`list` :raises :class:`recipe.definition.exceptions.InvalidDefinition`: If the definition contains a circular dependency """ if node.name in temp_set: msg = 'Recipe node \'%s\' has a circular dependency on itself' % node.name raise InvalidDefinition('CIRCULAR_DEPENDENCY', msg) if node.name not in perm_set: temp_set.add(node.name) for child_node in node.children.values(): self._topological_order_visit(child_node, results, perm_set, temp_set) perm_set.add(node.name) temp_set.remove(node.name) results.insert(0, node.name) return results
def validate(self, node_input_interfaces, node_output_interfaces): """Validates this recipe definition :param node_input_interfaces: The input interface for each job/recipe node stored by node name :type node_input_interfaces: dict :param node_output_interfaces: The output interface for each job node stored by node name :type node_output_interfaces: dict :returns: A list of warnings discovered during validation :rtype: :func:`list` :raises :class:`recipe.definition.exceptions.InvalidDefinition`: If the definition is invalid """ try: warnings = self.input_interface.validate() except InvalidInterface as ex: raise InvalidDefinition('INPUT_INTERFACE', ex.error.description) # Processing nodes in topological order will also detect any circular dependencies for node_name in self.get_topological_order(): node = self.graph[node_name] # Grab input and output interfaces from condition nodes if node.node_type == ConditionNodeDefinition.NODE_TYPE: node_input_interfaces[node_name] = node.input_interface node_output_interfaces[node_name] = node.output_interface warnings.extend( node.validate(self.input_interface, node_input_interfaces, node_output_interfaces)) return warnings
def __init__(self, definition=None, do_validate=False): """Creates a v6 recipe definition JSON object from the given dictionary :param definition: The recipe definition JSON dict :type definition: dict :param do_validate: Whether to perform validation on the JSON schema :type do_validate: bool :raises :class:`recipe.definition.exceptions.InvalidDdefinition`: If the given definition is invalid """ if not definition: definition = {} self._definition = copy.deepcopy(definition) if 'version' not in self._definition: self._definition['version'] = SCHEMA_VERSION if self._definition['version'] != SCHEMA_VERSION: self._convert_from_v1() self._populate_default_values() try: if do_validate: validate(self._definition, RECIPE_DEFINITION_SCHEMA) except ValidationError as ex: raise InvalidDefinition( 'INVALID_DEFINITION', 'Invalid recipe definition: %s' % unicode(ex))
def validate(self, recipe_input_interface, node_input_interfaces, node_output_interfaces): """Validates this node :param recipe_input_interface: The interface for the recipe input :type recipe_input_interface: :class:`data.interface.interface.Interface` :param node_input_interfaces: The input interface for each node stored by node name :type node_input_interfaces: dict :param node_output_interfaces: The output interface for each node stored by node name :type node_output_interfaces: dict :returns: A list of warnings discovered during validation :rtype: :func:`list` :raises :class:`recipe.definition.exceptions.InvalidDefinition`: If the definition is invalid """ warnings = [] input_interface = node_input_interfaces[self.name] connecting_interface = Interface() # Generate complete dependency set for this node all_dependencies = set() dependency_list = list(self.parents.values()) while dependency_list: dependency = dependency_list.pop() if dependency.name not in all_dependencies: all_dependencies.add(dependency.name) dependency_list.extend(list(dependency.parents.values())) try: for connection in self.connections.values(): # Validate each connection warnings.extend(connection.validate(all_dependencies)) # Combine all connections into a connecting interface warnings.extend( connection.add_parameter_to_interface( connecting_interface, recipe_input_interface, node_output_interfaces)) # Validate that connecting interface can be passed to this interface warnings.extend( input_interface.validate_connection(connecting_interface)) except InvalidInterfaceConnection as ex: error_desc = ex.error.description # Parse error message to try and give the user a better message. if error_desc.startswith("Parameter") and error_desc.endswith( "is required"): msg = 'input \'%s\' is missing for %s' % ( error_desc.split("\'")[1], self.name) else: msg = 'Node \'%s\' interface error: %s' % (self.name, error_desc) raise InvalidDefinition('NODE_INTERFACE', msg) return warnings
def __init__(self, definition=None, do_validate=False): """Creates a v6 recipe definition JSON object from the given dictionary :param definition: The recipe definition JSON dict :type definition: dict :param do_validate: Whether to perform validation on the JSON schema :type do_validate: bool :raises :class:`recipe.definition.exceptions.InvalidDefinition`: If the given definition is invalid """ if not definition: definition = {} self._definition = copy.deepcopy(definition) if 'version' not in self._definition: self._definition['version'] = SCHEMA_VERSION if self._definition['version'] not in SCHEMA_VERSIONS: self._convert_from_v1() self._populate_default_values() try: if do_validate: validate(self._definition, RECIPE_DEFINITION_SCHEMA) except ValidationError as ex: if type(ex.instance) == dict: node_name = ex.absolute_path[-2] else: node_name = ex.instance if len(ex.context) > 0: # Get the list of all sub-errors. Raise this error as a 'JSON' so that # the UI can display each error in their own error box. error_msg = json.dumps(['Issue with %s: %s' % (node_name, filter_out_us(m.message)) for m in ex.context]) raise InvalidDefinition('INVALID_DEFINITION_JSON', error_msg) else: raise InvalidDefinition('INVALID_DEFINITION', 'Issue with %s: %s' % (node_name, filter_out_us(ex.message)))
def _add_node(self, node): """Adds a node to the recipe graph :param node: The node :type node: :class:`recipe.definition.node.NodeDefinition` :raises :class:`recipe.definition.exceptions.InvalidDefinition`: If the node is duplicated """ if node.name in self.graph: raise InvalidDefinition('DUPLICATE_NODE', 'Node \'%s\' is already defined' % node.name) self.graph[node.name] = node self._topological_order = None # Invalidate cache
def _add_connection(self, node_name, connection): """Adds a connection to the input of the node :param node_name: The name of the node whose input is being connected to :type node_name: string :param connection: The connection to the node input :type connection: :class:`recipe.definition.connection.InputConnection` :raises :class:`recipe.definition.exceptions.InvalidDefinition`: If the node is unknown or the connection is a duplicate """ if node_name not in self.graph: raise InvalidDefinition('UNKNOWN_NODE', 'Node \'%s\' is not defined' % node_name) node = self.graph[node_name] node.add_connection(connection)
def add_connection(self, connection): """Adds a connection that connects a parameter to one of this node's inputs :param connection: The connection to add :type connection: :class:`recipe.definition.connection.InputConnection` :raises :class:`recipe.definition.exceptions.InvalidDefinition`: If the definition is invalid """ try: if connection.input_name in self.connections: msg = 'Input \'%s\' has more than one parameter connected to it' % connection.input_name raise InvalidInterfaceConnection('DUPLICATE_INPUT', msg) self.connections[connection.input_name] = connection except InvalidInterfaceConnection as ex: msg = 'Node \'%s\' interface error: %s' % (self.name, ex.error.description) raise InvalidDefinition('NODE_INTERFACE', msg)
def add_recipe_input_connection(self, node_name, node_input_name, recipe_input_name): """Adds a connection from a recipe input to the input of a node :param node_name: The name of the node whose input is being connected to :type node_name: string :param node_input_name: The name of the node's input :type node_input_name: string :param recipe_input_name: The name of the recipe input being connected to the node's input :type recipe_input_name: string :raises :class:`recipe.definition.exceptions.InvalidDefinition`: If the node or recipe input is unknown or the connection is a duplicate """ if recipe_input_name not in self.input_interface.parameters: raise InvalidDefinition('UNKNOWN_INPUT', 'Recipe input \'%s\' is not defined' % recipe_input_name) connection = RecipeInputConnection(node_input_name, recipe_input_name) self._add_connection(node_name, connection)