Esempio n. 1
0
class Action(ExecutionElement, Execution_Base):
    __tablename__ = 'action'
    workflow_id = Column(UUIDType(binary=False), ForeignKey('workflow.id'))
    app_name = Column(String(80), nullable=False)
    action_name = Column(String(80), nullable=False)
    name = Column(String(80), nullable=False)
    device_id = relationship('Argument', uselist=False, cascade='all, delete-orphan',
                             foreign_keys=[Argument.action_device_id])
    arguments = relationship('Argument', cascade='all, delete, delete-orphan', foreign_keys=[Argument.action_id])
    trigger = relationship('ConditionalExpression', cascade='all, delete-orphan', uselist=False)
    position = relationship('Position', uselist=False, cascade='all, delete-orphan')
    children = ('arguments', 'trigger')

    def __init__(self, app_name, action_name, name, device_id=None, id=None, arguments=None, trigger=None,
                 position=None):
        """Initializes a new Action object. A Workflow has one or more 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): The name of the Action object.
            device_id (Argument, optional): The device_id for the Action. This device_id is specified in the Argument
                object. If the device_id should be static, then device_id.value should be set to the static device_id.
                If the device_id should be fetched from a previous Action, then the reference and optional selection
                fields of the Argument object should be filled. Defaults to None.
            id (str|UUID, optional): Optional UUID to pass into the Action. Must be UUID object or valid UUID string.
                Defaults to None.
            arguments (list[Argument], optional): A list of Argument objects that are parameters to the action.
                Defaults to None.
            trigger (ConditionalExpression, optional): A ConditionalExpression which causes an Action to wait until the
                data is sent fulfilling the condition. Defaults to None.
            position (Position, optional): Position object for the Action. Defaults to None.
        """
        ExecutionElement.__init__(self, id)

        self.trigger = trigger

        self.name = name
        self.device_id = device_id
        self.app_name = app_name
        self.action_name = action_name

        self.arguments = []
        if arguments:
            self.arguments = arguments

        self.position = position

        self._run = None
        self._arguments_api = None
        self._output = None
        self._execution_id = 'default'
        self._action_executable = None
        self._resolved_device_id = -1
        self.validate()

    @orm.reconstructor
    def init_on_load(self):
        """Loads all necessary fields upon Action being loaded from database"""
        if not self.errors:
            errors = []
            try:
                self._run, self._arguments_api = get_app_action_api(self.app_name, self.action_name)
                self._action_executable = get_app_action(self.app_name, self._run)
            except UnknownApp:
                errors.append('Unknown app {}'.format(self.app_name))
            except UnknownAppAction:
                errors.append('Unknown app action {}'.format(self.action_name))
            self.errors = errors
        self._output = None
        self._execution_id = 'default'
        self._resolved_device_id = -1

    def validate(self):
        """Validates the object"""
        errors = []
        try:
            self._run, self._arguments_api = get_app_action_api(self.app_name, self.action_name)
            self._action_executable = get_app_action(self.app_name, self._run)
            if is_app_action_bound(self.app_name, self._run) and not self.device_id:
                message = 'App action is bound but no device ID was provided.'.format(self.name)
                errors.append(message)
            validate_app_action_parameters(self._arguments_api, self.arguments, self.app_name, self.action_name)
        except UnknownApp:
            errors.append('Unknown app {}'.format(self.app_name))
        except UnknownAppAction:
            errors.append('Unknown app action {}'.format(self.action_name))
        except InvalidArgument as e:
            errors.extend(e.errors)
        self.errors = errors

    def get_output(self):
        """Gets the output of an Action (the result)

        Returns:
            (ActionResult): The result of the Action
        """
        return self._output

    def get_execution_id(self):
        """Gets the execution ID of the Action

        Returns:
            (UUID): The execution ID
        """
        return self._execution_id

    def execute(self, accumulator, instance=None, arguments=None, resume=False):
        """Executes an Action by calling the associated app function.

        Args:
            accumulator (dict): Dict containing the results of the previous actions
            instance (App, optional): The instance of an App object to be used to execute the associated function.
                This field is required if the Action is a bounded action. Otherwise, it defaults to None.
            arguments (list[Argument], optional): List of Arguments to be used if the Action is the starting step of
                the Workflow. Defaults to None.
            resume (bool, optional): Optional boolean to resume a previously paused workflow. Defaults to False.

        Returns:
            (ActionResult): The result of the executed function.
        """
        logger.info('Executing action {} (id={})'.format(self.name, str(self.name)))
        self._execution_id = str(uuid.uuid4())

        if self.device_id:
            self._resolved_device_id = self.device_id.get_value(accumulator)
            logger.debug('Device resolved to {} for action {}'.format(self._resolved_device_id, str(self.id)))

        if arguments:
            WalkoffEvent.CommonWorkflowSignal.send(self, event=WalkoffEvent.ActionStarted,
                                                   data={'start_arguments': arguments})
        else:
            WalkoffEvent.CommonWorkflowSignal.send(self, event=WalkoffEvent.ActionStarted)

        if self.trigger and not resume:
            WalkoffEvent.CommonWorkflowSignal.send(self, event=WalkoffEvent.TriggerActionAwaitingData)
            logger.debug('Trigger Action {} is awaiting data'.format(self.name))
            self._output = None
            return ActionResult("trigger", "trigger")

        arguments = arguments if arguments else self.arguments

        try:
            args = validate_app_action_parameters(self._arguments_api, arguments, self.app_name, self.action_name,
                                                  accumulator=accumulator)
            if is_app_action_bound(self.app_name, self._run):
                result = self._action_executable(instance, **args)
            else:
                result = self._action_executable(**args)
            result.set_default_status(self.app_name, self.action_name)
            if result.is_failure(self.app_name, self.action_name):
                WalkoffEvent.CommonWorkflowSignal.send(self, event=WalkoffEvent.ActionExecutionError,
                                                       data=result.as_json())
            else:
                WalkoffEvent.CommonWorkflowSignal.send(self, event=WalkoffEvent.ActionExecutionSuccess,
                                                       data=result.as_json())
        except Exception as e:
            logger.exception('Error executing action {} (id={})'.format(self.name, str(self.id)))
            self.__handle_execution_error(e)
        else:
            self._output = result
            logger.debug(
                'Action {0}-{1} (id {2}) executed successfully'.format(self.app_name, self.action_name, self.id))
            return result

    def __handle_execution_error(self, e):
        formatted_error = format_exception_message(e)
        if isinstance(e, InvalidArgument):
            event = WalkoffEvent.ActionArgumentsInvalid
            return_type = 'InvalidArguments'
        else:
            event = WalkoffEvent.ActionExecutionError
            return_type = 'UnhandledException'
        self._output = ActionResult('error: {0}'.format(formatted_error), return_type)
        WalkoffEvent.CommonWorkflowSignal.send(self, event=event, data=self._output.as_json())

    def execute_trigger(self, data_in, accumulator):
        """Executes the trigger for an Action, which will continue execution if the trigger returns True

        Args:
            data_in (dict): The data to send to the trigger to test against
            accumulator (dict): Dict containing the results of the previous actions

        Returns:
            (bool): True if the trigger returned True, False otherwise
        """
        if self.trigger.execute(data_in=data_in, accumulator=accumulator):
            logger.debug('Trigger is valid for input {0}'.format(data_in))
            return True
        else:
            logger.debug('Trigger is not valid for input {0}'.format(data_in))
            return False

    def get_resolved_device_id(self):
        return self._resolved_device_id
Esempio n. 2
0
class Action(ExecutionElement, Device_Base):
    __tablename__ = 'action'
    workflow_id = Column(UUIDType(binary=False), ForeignKey('workflow.id'))
    app_name = Column(String(80), nullable=False)
    action_name = Column(String(80), nullable=False)
    name = Column(String(80), nullable=False)
    device_id = Column(Integer)
    arguments = relationship('Argument', cascade='all, delete, delete-orphan')
    trigger = relationship('ConditionalExpression',
                           cascade='all, delete-orphan',
                           uselist=False)
    position = relationship('Position',
                            uselist=False,
                            cascade='all, delete-orphan')

    def __init__(self,
                 app_name,
                 action_name,
                 name,
                 device_id=None,
                 id=None,
                 arguments=None,
                 trigger=None,
                 position=None):
        """Initializes a new Action object. A Workflow has one or more 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): The name of the Action object.
            device_id (int, optional): The id of the device associated with the app associated with the Action. Defaults
                to None.
            id (str|UUID, optional): Optional UUID to pass into the Action. Must be UUID object or valid UUID string.
                Defaults to None.
            arguments (list[Argument], optional): A list of Argument objects that are parameters to the action.
                Defaults to None.
            trigger (ConditionalExpression, optional): A ConditionalExpression which causes an Action to wait until the
                data is sent fulfilling the condition. Defaults to None.
            position (Position, optional): Position object for the Action. Defaults to None.
        """
        ExecutionElement.__init__(self, id)

        self.trigger = trigger

        self.name = name
        self.device_id = device_id
        self.app_name = app_name
        self.action_name = action_name

        self.arguments = []
        if arguments:
            self.arguments = arguments

        self.position = position

        self._run = None
        self._arguments_api = None
        self._output = None
        self._execution_id = 'default'

        self.validate()
        self._action_executable = get_app_action(self.app_name, self._run)

    @orm.reconstructor
    def init_on_load(self):
        """Loads all necessary fields upon Action being loaded from database"""
        self._run, self._arguments_api = get_app_action_api(
            self.app_name, self.action_name)
        self._output = None
        self._action_executable = get_app_action(self.app_name, self._run)
        self._execution_id = 'default'

    def validate(self):
        errors = {}
        try:
            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:
                message = 'App action is bound but no device ID was provided.'.format(
                    self.name)
                errors['executable'] = message
            validate_app_action_parameters(self._arguments_api, self.arguments,
                                           self.app_name, self.action_name)
        except UnknownApp:
            errors['executable'] = 'Unknown app {}'.format(self.app_name)
        except UnknownAppAction:
            errors['executable'] = 'Unknown app action {}'.format(
                self.action_name)
        except InvalidArgument as e:
            errors['arguments'] = e.errors
        if errors:
            raise InvalidExecutionElement(self.id,
                                          self.action_name,
                                          'Invalid action {}'.format(
                                              self.id or self.action_name),
                                          errors=[errors])

    def get_output(self):
        """Gets the output of an Action (the result)
        Returns:
            The result of the Action
        """
        return self._output

    def get_execution_id(self):
        """Gets the execution ID of the Action
        Returns:
            The execution ID
        """
        return self._execution_id

    def set_arguments(self, new_arguments):
        """Updates the arguments for an Action object.
        Args:
            new_arguments ([Argument]): The new Arguments for the Action object.
        """
        validate_app_action_parameters(self._arguments_api, new_arguments,
                                       self.app_name, self.action_name)
        self.arguments = new_arguments

    def execute(self, instance, accumulator, arguments=None, resume=False):
        """Executes an Action 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 actions
            arguments (list[Argument]): Optional list of Arguments to be used if the Action is the starting step of
                the Workflow. Defaults to None.
            resume (bool, optional): Optional boolean to resume a previously paused workflow. Defaults to False.
        Returns:
            The result of the executed function.
        """
        self._execution_id = str(uuid.uuid4())

        WalkoffEvent.CommonWorkflowSignal.send(
            self, event=WalkoffEvent.ActionStarted)
        if self.trigger and not resume:
            WalkoffEvent.CommonWorkflowSignal.send(
                self, event=WalkoffEvent.TriggerActionAwaitingData)
            logger.debug('Trigger Action {} is awaiting data'.format(
                self.name))
            self._output = None
            return ActionResult("trigger", "trigger")

        arguments = arguments if arguments else self.arguments

        try:
            args = validate_app_action_parameters(self._arguments_api,
                                                  arguments,
                                                  self.app_name,
                                                  self.action_name,
                                                  accumulator=accumulator)
            if is_app_action_bound(self.app_name, self._run):
                result = self._action_executable(instance, **args)
            else:
                result = self._action_executable(**args)
            result.set_default_status(self.app_name, self.action_name)
            if result.is_failure(self.app_name, self.action_name):
                WalkoffEvent.CommonWorkflowSignal.send(
                    self,
                    event=WalkoffEvent.ActionExecutionError,
                    data=result.as_json())
            else:
                WalkoffEvent.CommonWorkflowSignal.send(
                    self,
                    event=WalkoffEvent.ActionExecutionSuccess,
                    data=result.as_json())
        except Exception as e:
            self.__handle_execution_error(e)
        else:
            self._output = result
            logger.debug(
                'Action {0}-{1} (id {2}) executed successfully'.format(
                    self.app_name, self.action_name, self.id))
            return result

    def __handle_execution_error(self, e):
        formatted_error = format_exception_message(e)
        if isinstance(e, InvalidArgument):
            event = WalkoffEvent.ActionArgumentsInvalid
            return_type = 'InvalidArguments'
        else:
            event = WalkoffEvent.ActionExecutionError
            return_type = 'UnhandledException'
        logger.warning('Exception in {0}: \n{1}'.format(
            self.name, traceback.format_exc()))
        logger.error('Error calling action {0}. Error: {1}'.format(
            self.name, formatted_error))
        self._output = ActionResult('error: {0}'.format(formatted_error),
                                    return_type)
        WalkoffEvent.CommonWorkflowSignal.send(self,
                                               event=event,
                                               data=self._output.as_json())

    def execute_trigger(self, data_in, accumulator):
        """Executes the trigger for an Action, which will continue execution if the trigger returns True
        Args:
            data_in (dict): The data to send to the trigger to test against
            accumulator (dict): Dict containing the results of the previous actions
        Returns:
            True if the trigger returned True, False otherwise
        """
        if self.trigger.execute(data_in=data_in, accumulator=accumulator):
            logger.debug('Trigger is valid for input {0}'.format(data_in))
            return True
        else:
            logger.debug('Trigger is not valid for input {0}'.format(data_in))
            return False