def add_reaction(self, state, event, reaction, *args, **kwargs):
        """Adds a reaction that may get triggered by the given event & state.

        Reaction callbacks may (depending on how the state machine is ran) be
        used after an event is processed (and a transition occurs) to cause the
        machine to react to the newly arrived at stable state.

        These callbacks are expected to accept three default positional
        parameters (although more can be passed in via *args and **kwargs,
        these will automatically get provided to the callback when it is
        activated *ontop* of the three default). The three default parameters
        are the last stable state, the new stable state and the event that
        caused the transition to this new stable state to be arrived at.

        The expected result of a callback is expected to be a new event that
        the callback wants the state machine to react to. This new event
        may (depending on how the state machine is ran) get processed (and
        this process typically repeats) until the state machine reaches a
        terminal state.
        """
        if self.frozen:
            raise excp.FrozenMachine()
        if state not in self._states:
            raise excp.NotFound("Can not add a reaction to event '%s' for an"
                                " undefined state '%s'" % (event, state))
        if not six.callable(reaction):
            raise ValueError("Reaction callback must be callable")
        if event not in self._states[state]['reactions']:
            self._states[state]['reactions'][event] = (reaction, args, kwargs)
        else:
            raise excp.Duplicate("State '%s' reaction to event '%s'"
                                 " already defined" % (state, event))
    def add_state(self, state, terminal=False, on_enter=None, on_exit=None):
        """Adds a given state to the state machine.

        The ``on_enter`` and ``on_exit`` callbacks, if provided will be
        expected to take two positional parameters, these being the state
        being exited (for ``on_exit``) or the state being entered (for
        ``on_enter``) and a second parameter which is the event that is
        being processed that caused the state transition.
        """
        if self.frozen:
            raise excp.FrozenMachine()
        if state in self._states:
            raise excp.Duplicate("State '%s' already defined" % state)
        if on_enter is not None:
            if not six.callable(on_enter):
                raise ValueError("On enter callback must be callable")
        if on_exit is not None:
            if not six.callable(on_exit):
                raise ValueError("On exit callback must be callable")
        self._states[state] = {
            'terminal': bool(terminal),
            'reactions': {},
            'on_enter': on_enter,
            'on_exit': on_exit,
        }
        self._transitions[state] = collections.OrderedDict()
    def add_transition(self, start, end, event, replace=False):
        """Adds an allowed transition from start -> end for the given event.

        :param start: starting state
        :param end: ending state
        :param event: event that causes start state to
                      transition to end state
        :param replace: replace existing event instead of raising a
                        :py:class:`~automaton.exceptions.Duplicate` exception
                        when the transition already exists.
        """
        if self.frozen:
            raise excp.FrozenMachine()
        if start not in self._states:
            raise excp.NotFound("Can not add a transition on event '%s' that"
                                " starts in a undefined state '%s'" %
                                (event, start))
        if end not in self._states:
            raise excp.NotFound("Can not add a transition on event '%s' that"
                                " ends in a undefined state '%s'" %
                                (event, end))
        if self._states[start]['terminal']:
            raise excp.InvalidState("Can not add a transition on event '%s'"
                                    " that starts in the terminal state '%s'" %
                                    (event, start))
        if event in self._transitions[start] and not replace:
            target = self._transitions[start][event]
            if target.name != end:
                raise excp.Duplicate(
                    "Cannot add transition from"
                    " '%(start_state)s' to '%(end_state)s'"
                    " on event '%(event)s' because a"
                    " transition from '%(start_state)s'"
                    " to '%(existing_end_state)s' on"
                    " event '%(event)s' already exists." % {
                        'existing_end_state': target.name,
                        'end_state': end,
                        'event': event,
                        'start_state': start
                    })
        else:
            target = _Jump(end, self._states[end]['on_enter'],
                           self._states[start]['on_exit'])
            self._transitions[start][event] = target