def __init__(self, xml=None, name='', action='', app='', device='', inputs=None, next_steps=None, errors=None, parent_name='', position=None, ancestry=None, widgets=None, risk=0): """Initializes a new Step object. A Workflow has many steps that it executes. Args: xml (ElementTree, optional): The XML element tree object. Defaults to None. name (str, optional): The name of the Step object. Defaults to an empty string. action (str, optional): The name of the action associated with a Step. Defaults to an empty string. app (str, optional): The name of the app associated with the Step. Defaults to an empty string. device (str, optional): The name of the device associated with the app associated with the Step. Defaults to an empty string. inputs (dict, optional): A dictionary of Argument objects that are input to the step execution. Defaults to None. next_steps (list[NextStep], optional): A list of NextStep objects for the Step object. Defaults to None. errors (list[NextStep], optional): A list of NextStep error objects for the Step object. Defaults to None. parent_name (str, optional): The name of the parent for ancestry purposes. Defaults to an empty string. position (dict, optional): A dictionary with the x and y coordinates of the Step object. This is used for UI display purposes. Defaults to None. ancestry (list[str], optional): The ancestry for the Step object. Defaults to None. widgets (list[tuple(str, str)], optional): A list of widget tuples, which holds the app and the corresponding widget. Defaults to None. risk (int, optional): The risk associated with the Step. Defaults to 0. """ ExecutionElement.__init__(self, name=name, parent_name=parent_name, ancestry=ancestry) if xml is not None: self._from_xml(xml, parent_name=parent_name, ancestry=ancestry) else: if action == '' or app == '': raise InvalidElementConstructed('Either both action and app or xml must be ' 'specified in step constructor') self.action = action self.app = app self.run, self.input_api = get_app_action_api(self.app, self.action) get_app_action(self.app, self.run) inputs = inputs if inputs is not None else {} self.input = validate_app_action_parameters(self.input_api, inputs, self.app, self.action) self.device = device self.risk = risk self.conditionals = next_steps if next_steps is not None else [] self.errors = errors if errors is not None else [] self.position = position if position is not None else {} self.widgets = [_Widget(widget_app, widget_name) for (widget_app, widget_name) in widgets] if widgets is not None else [] self.raw_xml = self.to_xml() self.templated = False self.output = None self.next_up = None
def _from_xml(self, step_xml, parent_name='', ancestry=None): self.raw_xml = step_xml name = step_xml.get('id') ExecutionElement.__init__(self, name=name, parent_name=parent_name, ancestry=ancestry) self.action = step_xml.find('action').text self.app = step_xml.find('app').text self.run, self.input_api = get_app_action_api(self.app, self.action) is_templated_xml = step_xml.find('templated') self.templated = is_templated_xml is not None and bool( is_templated_xml.text) get_app_action(self.app, self.run) input_xml = step_xml.find('inputs') if input_xml is not None: inputs = inputs_xml_to_dict(input_xml) or {} if not self.templated: self.input = validate_app_action_parameters( self.input_api, inputs, self.app, self.action) else: self.input = inputs else: self.input = validate_app_action_parameters( self.input_api, {}, self.app, self.action) device_field = step_xml.find('device') self.device = device_field.text if device_field is not None else '' risk_field = step_xml.find('risk') self.risk = risk_field.text if risk_field is not None else 0 self.conditionals = [ nextstep.NextStep(xml=next_step_element, parent_name=self.name, ancestry=self.ancestry) for next_step_element in step_xml.findall('next') ] self.widgets = [ _Widget(widget.get('app'), widget.text) for widget in step_xml.findall('widgets/*') ] position = step_xml.find('position') if position is None: self.position = {} else: x_position = position.find('x') y_position = position.find('y') if x_position is not None and y_position is not None: self.position = {'x': x_position.text, 'y': y_position.text} else: self.position = {}
def validate_actions(actions, dereferencer, app_name): from apps import get_all_actions_for_app, get_app_action defined_actions = get_all_actions_for_app(app_name) seen = set() for action_name, action in actions.items(): if action['run'] not in defined_actions: raise InvalidApi('Action {0} has "run" property {1} ' 'which is not defined in App {2}'.format( action_name, action['run'], app_name)) action = dereferencer(action) action_params = dereferencer(action.get('parameters', [])) event = action.get('event', '') if action_params: validate_action_params(action_params, dereferencer, app_name, action_name, get_app_action(app_name, action['run']), event=event) validate_app_action_return_codes(action.get('returns', []), app_name, action_name) seen.add(action['run']) if seen != set(defined_actions.keys()): logger.warning( 'App {0} has defined the following actions which do not have a corresponding API: ' '{1}'.format(app_name, (set(defined_actions.keys()) - seen)))
def execute(self, instance, accumulator): """Executes a Step by calling the associated app function. Args: instance (App): The instance of an App object to be used to execute the associated function. accumulator (dict): Dict containing the results of the previous steps Returns: The result of the executed function. """ callbacks.StepInputValidated.send(self) try: args = dereference_step_routing(self.input, accumulator, 'In step {0}'.format(self.name)) args = validate_app_action_parameters(self.input_api, args, self.app, self.action) action = get_app_action(self.app, self.run) result = action(instance, **args) callbacks.FunctionExecutionSuccess.send(self, data=json.dumps({"result": result})) except InvalidInput as e: logger.error('Error calling step {0}. Error: {1}'.format(self.name, str(e))) callbacks.StepInputInvalid.send(self) self.output = 'error: {0}'.format(str(e)) raise except Exception as e: logger.error('Error calling step {0}. Error: {1}'.format(self.name, str(e))) self.output = 'error: {0}'.format(str(e)) raise else: self.output = result for widget in self.widgets: get_widget_signal(widget.app, widget.widget).send(self, data=json.dumps({"result": result})) logger.debug('Step {0} executed successfully'.format(self.ancestry)) return result
def test_validate_action_params_too_few_params_in_api(self): self.basicapi['actions']['Add Three'] = {'run': 'main.Main.addThree', 'parameters': [{'name': 'num1', 'type': 'number'}, {'name': 'num2', 'type': 'number'}]} self.__generate_resolver_dereferencer(self.basicapi) with self.assertRaises(InvalidApi): validate_action_params(self.basicapi['actions']['Add Three']['parameters'], self.dereferencer, 'HelloWorldBounded', 'Add Three', get_app_action('HelloWorldBounded', 'main.Main.addThree'))
def test_validate_action_params_no_duplicate_params_matches_signature(self): self.basicapi['actions']['Add Three'] = {'run': 'main.Main.addThree', 'parameters': [{'name': 'num1', 'type': 'number'}, {'name': 'num2', 'type': 'number'}, {'name': 'num3', 'type': 'number'}]} self.__generate_resolver_dereferencer(self.basicapi) validate_action_params(self.basicapi['actions']['Add Three']['parameters'], self.dereferencer, 'HelloWorldBounded', 'Add Three', get_app_action('HelloWorldBounded', 'main.Main.addThree'))
def test_validate_action_params_duplicate_param_name(self): self.basicapi['actions']['Add Three'] = {'run': 'main.Main.addThree', 'parameters': [{'name': 'num1', 'type': 'number'}, {'name': 'num1', 'type': 'string'}, {'name': 'num2', 'type': 'number'}]} self.__generate_resolver_dereferencer(self.basicapi, expected_success=False) with self.assertRaises(InvalidApi): validate_action_params(self.basicapi['actions']['Add Three']['parameters'], self.dereferencer, 'HelloWorldBounded', 'Add Three', get_app_action('HelloWorldBounded', 'main.Main.addThree'))
def test_validate_action_params_event(self): self.basicapi['actions']['Sample Event'] = { 'run': 'main.Main.sample_event', 'event': 'Event1', 'parameters': [ { 'name': 'arg1', 'type': 'number' }, ] } self.__generate_resolver_dereferencer(self.basicapi) validate_action_params( self.basicapi['actions']['Sample Event']['parameters'], self.dereferencer, 'HelloWorld', 'Sample Event', get_app_action('HelloWorld', 'main.Main.sample_event'), event='event1')
def __init__(self, app, action, name='', device='', inputs=None, triggers=None, next_steps=None, position=None, widgets=None, risk=0, uid=None, templated=False, raw_representation=None): """Initializes a new Step object. A Workflow has many steps that it executes. Args: app (str): The name of the app associated with the Step action (str): The name of the action associated with a Step name (str, optional): The name of the Step object. Defaults to an empty string. device (str, optional): The name of the device associated with the app associated with the Step. Defaults to an empty string. inputs (dict, optional): A dictionary of Argument objects that are input to the step execution. Defaults to None. triggers (list[Flag], optional): A list of Flag objects for the Step. If a Step should wait for data before continuing, then include these Trigger objects in the Step init. Defaults to None. next_steps (list[NextStep], optional): A list of NextStep objects for the Step object. Defaults to None. position (dict, optional): A dictionary with the x and y coordinates of the Step object. This is used for UI display purposes. Defaults to None. widgets (list[tuple(str, str)], optional): A list of widget tuples, which holds the app and the corresponding widget. Defaults to None. risk (int, optional): The risk associated with the Step. Defaults to 0. uid (str, optional): A universally unique identifier for this object. Created from uuid.uuid4().hex in Python templated (bool, optional): Whether or not the Step is templated. Used for Jinja templating. raw_representation (dict, optional): JSON representation of this object. Used for Jinja templating. """ ExecutionElement.__init__(self, uid) self.triggers = triggers if triggers is not None else [] self._incoming_data = AsyncResult() self.name = name self.app = app self.action = action self._run, self._input_api = get_app_action_api(self.app, self.action) get_app_action(self.app, self._run) if isinstance(inputs, list): inputs = {arg['name']: arg['value'] for arg in inputs} elif isinstance(inputs, dict): inputs = inputs else: inputs = {} self.templated = templated if not self.templated: self.inputs = validate_app_action_parameters( self._input_api, inputs, self.app, self.action) else: self.inputs = inputs self.device = device if (device is not None and device != 'None') else '' self.risk = risk self.next_steps = next_steps if next_steps is not None else [] self.position = position if position is not None else {} self.widgets = [ widget if isinstance(widget, Widget) else Widget(**widget) for widget in widgets ] if widgets is not None else [] self._output = None self._next_up = None self._raw_representation = raw_representation if raw_representation is not None else {} self._execution_uid = 'default'
def execute(self, instance, accumulator): """Executes a Step by calling the associated app function. Args: instance (App): The instance of an App object to be used to execute the associated function. accumulator (dict): Dict containing the results of the previous steps Returns: The result of the executed function. """ self._execution_uid = uuid.uuid4().hex data_sent.send(self, callback_name="Step Started", object_type="Step") if self.triggers: data_sent.send(self, callback_name="Trigger Step Awaiting Data", object_type="Step") logger.debug('Trigger Step {} is awaiting data'.format(self.name)) while True: try: data = self._incoming_data.get(timeout=1) self._incoming_data = AsyncResult() except gevent.Timeout: gevent.sleep(0.1) continue data_in = data['data_in'] inputs = data['inputs'] if 'inputs' in data else {} if all( flag.execute(data_in=data_in, accumulator=accumulator) for flag in self.triggers): data_sent.send(self, callback_name="Trigger Step Taken", object_type="Step") logger.debug( 'Trigger is valid for input {0}'.format(data_in)) accumulator[self.name] = data_in if inputs: self.inputs.update(inputs) break else: logger.debug( 'Trigger is not valid for input {0}'.format(data_in)) data_sent.send(self, callback_name="Trigger Step Not Taken", object_type="Step") gevent.sleep(0.1) try: args = dereference_step_routing(self.inputs, accumulator, 'In step {0}'.format(self.name)) args = validate_app_action_parameters(self._input_api, args, self.app, self.action) action = get_app_action(self.app, self._run) if is_app_action_bound(self.app, self._run): result = action(instance, **args) else: result = action(**args) data_sent.send(self, callback_name="Function Execution Success", object_type="Step", data=json.dumps({"result": result.as_json()})) except InvalidInput as e: formatted_error = format_exception_message(e) logger.error('Error calling step {0}. Error: {1}'.format( self.name, formatted_error)) data_sent.send(self, callback_name="Step Input Invalid", object_type="Step") self._output = ActionResult('error: {0}'.format(formatted_error), 'InvalidInput') raise except Exception as e: formatted_error = format_exception_message(e) logger.error('Error calling step {0}. Error: {1}'.format( self.name, formatted_error)) self._output = ActionResult('error: {0}'.format(formatted_error), 'UnhandledException') raise else: self._output = result for widget in self.widgets: get_widget_signal(widget.app, widget.name).send( self, data=json.dumps({"result": result.as_json()})) logger.debug('Step {0}-{1} (uid {2}) executed successfully'.format( self.app, self.action, self.uid)) return result
def __init__(self, app_name, action_name, name='', device_id=None, arguments=None, triggers=None, position=None, uid=None, templated=False, raw_representation=None): """Initializes a new Action object. A Workflow has many actions that it executes. Args: app_name (str): The name of the app associated with the Action action_name (str): The name of the action associated with a Action name (str, optional): The name of the Action object. Defaults to an empty string. device_id (int, optional): The id of the device associated with the app associated with the Action. Defaults to None. arguments ([Argument], optional): A list of Argument objects that are parameters to the action. Defaults to None. triggers (list[Flag], optional): A list of Flag objects for the Action. If a Action should wait for data before continuing, then include these Trigger objects in the Action init. Defaults to None. position (dict, optional): A dictionary with the x and y coordinates of the Action object. This is used for UI display purposes. Defaults to None. uid (str, optional): A universally unique identifier for this object. Created from uuid.uuid4().hex in Python templated (bool, optional): Whether or not the Action is templated. Used for Jinja templating. raw_representation (dict, optional): JSON representation of this object. Used for Jinja templating. """ ExecutionElement.__init__(self, uid) self.triggers = triggers if triggers is not None else [] self._incoming_data = None self._event = threading.Event() self.name = name self.device_id = device_id self.app_name = app_name self.action_name = action_name self._run, self._arguments_api = get_app_action_api( self.app_name, self.action_name) if is_app_action_bound(self.app_name, self._run) and not self.device_id: raise InvalidArgument( "Cannot initialize Action {}. App action is bound but no device ID was provided." .format(self.name)) self._action_executable = get_app_action(self.app_name, self._run) arguments = {argument.name: argument for argument in arguments } if arguments is not None else {} self.templated = templated if not self.templated: validate_app_action_parameters(self._arguments_api, arguments, self.app_name, self.action_name) self.arguments = arguments self.position = position if position is not None else {} self._output = None self._raw_representation = raw_representation if raw_representation is not None else {} self._execution_uid = 'default'