Michael Pasternak has uploaded a new change for review.

Change subject: cli: introducing FiniteStateMachine
......................................................................

cli: introducing FiniteStateMachine

This patch moves shell state management to the
Deterministic Finite State Machine (DFSA).

Change-Id: Ic3ea3cf7fa82eb82c2323d96231de0a5e1094056
Signed-off-by: Michael pasternak <[email protected]>
---
M src/cli/error.py
M src/ovirtcli/__init__.py
M src/ovirtcli/annotations/requires.py
A src/ovirtcli/annotations/singleton.py
M src/ovirtcli/command/connect.py
M src/ovirtcli/command/disconnect.py
D src/ovirtcli/listeners/exitlistener.py
A src/ovirtcli/meta/__init__.py
A src/ovirtcli/meta/singleton.py
M src/ovirtcli/shell/engineshell.py
A src/ovirtcli/state/__init__.py
A src/ovirtcli/state/dfsaevent.py
A src/ovirtcli/state/dfsaeventcontext.py
A src/ovirtcli/state/dfsastate.py
A src/ovirtcli/state/finitestatemachine.py
A src/ovirtcli/state/statemachine.py
16 files changed, 884 insertions(+), 77 deletions(-)


  git pull ssh://gerrit.ovirt.org:29418/ovirt-engine-cli refs/changes/51/20451/1

diff --git a/src/cli/error.py b/src/cli/error.py
index 27f4740..013317c 100644
--- a/src/cli/error.py
+++ b/src/cli/error.py
@@ -16,7 +16,10 @@
 
 
 from cli import compat
+import types
 
+from ovirtcli.annotations.requires import Requires
+from ovirtcli.state.dfsastate import DFSAState
 
 class Error(Exception):
     """Base class for python-cli errors."""
@@ -40,3 +43,46 @@
 
 class SyntaxError(Error):  # @ReservedAssignment
     """Illegal syntax."""
+
+class StateError(Error):
+    """raised when state change error occurs."""
+#     @Requires([DFSAEvent, types.StringType])
+    def __init__(self, destination, current):
+        """
+        @param destination: the destination DFSAEvent
+        @param current: the current state
+        """
+        super(StateError, self).__init__(
+             message=
+             (
+                '\n\nMoving to the "%s" state is not allowed,\n'
+                %
+                str(DFSAState(destination.get_destination()))
+             )
+             +
+             (
+                'eligible states from the "%s" state are:\n%s'
+                %
+                (
+                    str(DFSAState(current)),
+                    str([ str(DFSAState(src))
+                      for src in destination.get_sources()
+                      ]
+                    )
+                 )
+             )
+        )
+
+class UnknownEventError(Error):
+    """raised when unregistered event is triggered."""
+    @Requires(types.StringType)
+    def __init__(self, name):
+        """
+        @param name: the name of DFSAEvent
+        """
+        super(UnknownEventError, self).__init__(
+             message=(
+                'Event %s, was not properly registered.' % \
+                name
+             )
+        )
diff --git a/src/ovirtcli/__init__.py b/src/ovirtcli/__init__.py
index 792d600..e69de29 100644
--- a/src/ovirtcli/__init__.py
+++ b/src/ovirtcli/__init__.py
@@ -1 +0,0 @@
-#
diff --git a/src/ovirtcli/annotations/requires.py 
b/src/ovirtcli/annotations/requires.py
index 5e8c20d..a87e32e 100644
--- a/src/ovirtcli/annotations/requires.py
+++ b/src/ovirtcli/annotations/requires.py
@@ -13,36 +13,78 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import types
 
 
 class Requires(object):
     """
     Checks that method arg is of a given type
+
+    @note:
+
+    1. checks that method's argument of type DFSAEvent
+
+        @Requires(DFSAEvent)
+        def method1(self, event):
+            ...
+
+    2. checks that method's argument is a List of DFSAEvent
+
+        @Requires([DFSAEvent])
+        def method2(self, events):
+            ...
     """
 
-    def __init__(self, typ):
+    def __init__(self, types_to_check):
         """
         Checks that method arg is of a given type
 
-        @param typ: the type to validate against
+        @param types_to_check: the types to validate against        
+        @note:
+
+        1. checks that method's argument of type DFSAEvent
+    
+            @Requires(DFSAEvent)
+            def method1(self, event):
+                ...
+    
+        2. checks that method's argument is a List of DFSAEvent
+    
+            @Requires([DFSAEvent])
+            def method2(self, events):
+                ...
         """
-        assert typ != None
-        self.typ = typ
+        assert types_to_check != None
+        self.__types_to_check = types_to_check
 
     def __call__(self, original_func):
         decorator_self = self
         def wrappee(*args, **kwargs):
             self.__check_list(
-                      args[1],
-                      decorator_self.typ
+                      args[1:],
+                      decorator_self.__types_to_check
             )
             return original_func(*args, **kwargs)
         return wrappee
 
-    def __check_list(self, candidate, typ):
-        if not isinstance(candidate, typ):
-            raise TypeError(
-                    "%s instance is expected."
-                    %
-                    typ.__name__
-            )
+    def __raise_error(self, typ):
+        raise TypeError(
+                "%s instance is expected."
+                %
+                typ.__name__
+        )
+
+    def __check_list(self, candidates, typs):
+        if isinstance(typs, types.ListType):
+            if type(candidates[0]) is types.ListType:
+                # the list of items of a specific type
+                if candidates[0]:
+                    for candidate in candidates[0]:
+                        if not isinstance(candidate, typs[0]):
+                            self.__raise_error(typs[0])
+                else:
+                    self.__raise_error(types.ListType)
+        else:
+            # the items is of a specific type
+            if not isinstance(candidates[0], typs):
+                self.__raise_error(typs)
diff --git a/src/ovirtcli/annotations/singleton.py 
b/src/ovirtcli/annotations/singleton.py
new file mode 100644
index 0000000..04b0c62
--- /dev/null
+++ b/src/ovirtcli/annotations/singleton.py
@@ -0,0 +1,29 @@
+#
+# Copyright (c) 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#           http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+def Singleton(clazz):
+    """
+    The singleton annotation
+
+    applying this annotation on class, will make sure that no
+    more than single instance of this class can be created.
+    """
+    instances = {}
+    def fetch_instance(*args, **kwargs):
+        if clazz not in instances:
+            instances[clazz] = clazz(*args, **kwargs)
+        return instances[clazz]
+    return fetch_instance
diff --git a/src/ovirtcli/command/connect.py b/src/ovirtcli/command/connect.py
index 68462c4..0c7a27c 100644
--- a/src/ovirtcli/command/connect.py
+++ b/src/ovirtcli/command/connect.py
@@ -19,12 +19,12 @@
 
 from ovirtcli.command.command import OvirtCommand
 from ovirtsdk.api import API
-from ovirtcli.settings import OvirtCliSettings
 from ovirtsdk.infrastructure.errors import RequestError, NoCertificatesError, \
     ConnectionError
 from cli.messages import Messages
 from urlparse import urlparse
 from ovirtcli.shell.connectcmdshell import ConnectCmdShell
+from ovirtcli.state.statemachine import StateMachine
 
 class ConnectCommand(OvirtCommand):
 
@@ -115,6 +115,8 @@
             )
 
         try:
+            StateMachine.connecting()  # @UndefinedVariable
+
             self.context.set_connection (
                      API(
                          url=url,
@@ -137,11 +139,7 @@
             if context.sdk_version < MIN_FORCE_CREDENTIALS_CHECK_VERSION:
                 self.__test_connectivity()
 
-            self.context.history.enable()
-            self.write(
-                   OvirtCliSettings.CONNECTED_TEMPLATE % \
-                   self.context.settings.get('ovirt-shell:version')
-            )
+            StateMachine.connected()  # @UndefinedVariable
 
         except RequestError, e:
             self.__cleanContext()
diff --git a/src/ovirtcli/command/disconnect.py 
b/src/ovirtcli/command/disconnect.py
index 989e3e5..762e285 100644
--- a/src/ovirtcli/command/disconnect.py
+++ b/src/ovirtcli/command/disconnect.py
@@ -16,10 +16,10 @@
 
 
 from ovirtcli.command.command import OvirtCommand
-from cli.context import ExecutionContext
-from ovirtcli.settings import OvirtCliSettings
-from cli.messages import Messages
+from ovirtcli.state.statemachine import StateMachine
 
+from cli.context import ExecutionContext
+from cli.messages import Messages
 
 class DisconnectCommand(OvirtCommand):
 
@@ -45,12 +45,12 @@
             )
             return
         try:
+            StateMachine.disconnecting()  # @UndefinedVariable
+
             self.context._clean_settings()
             connection.disconnect()
             self.context.status = ExecutionContext.OK
         except Exception:
             self.context.status = ExecutionContext.COMMAND_ERROR
         finally:
-            self.context.history.disable()
-            self.write(OvirtCliSettings.DISCONNECTED_TEMPLATE)
-            self.context.connection = None
+            StateMachine.disconnected()  # @UndefinedVariable
diff --git a/src/ovirtcli/listeners/exitlistener.py 
b/src/ovirtcli/listeners/exitlistener.py
deleted file mode 100644
index 65fee27..0000000
--- a/src/ovirtcli/listeners/exitlistener.py
+++ /dev/null
@@ -1,43 +0,0 @@
-#
-# Copyright (c) 2010 Red Hat, Inc.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#           http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-from ovirtcli.listeners.abstractlistener import AbstractListener
-from ovirtcli.shell.disconnectcmdshell import DisconnectCmdShell
-
-class ExitListener(AbstractListener):
-    '''
-    Listens for the exit events
-    '''
-
-    def __init__(self, shell):
-        """
-        @param shell: EngineShell instance
-        """
-        assert shell != None
-        self.__shell = shell
-
-    def onEvent(self, *args, **kwargs):
-        '''
-        fired when exit event is raised
-
-        @param args: a list o args
-        @param kwargs: a list o kwargs
-        '''
-
-        if self.__shell.context.connection:
-            self.__shell.onecmd(
-                        DisconnectCmdShell.NAME + "\n"
-            )
diff --git a/src/ovirtcli/meta/__init__.py b/src/ovirtcli/meta/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/ovirtcli/meta/__init__.py
diff --git a/src/ovirtcli/meta/singleton.py b/src/ovirtcli/meta/singleton.py
new file mode 100644
index 0000000..0c949e5
--- /dev/null
+++ b/src/ovirtcli/meta/singleton.py
@@ -0,0 +1,32 @@
+#
+# Copyright (c) 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#           http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+class Singleton(type):
+    """
+    The singleton annotation
+
+    applying this annotation on class, will make sure that no
+    more than single instance of this class can be created.
+    """
+    __all_instances = {}
+    def __call__(cls, *args, **kwargs):  # @NoSelf
+        if cls not in cls.__all_instances:
+            cls.__all_instances[cls] = super(
+                         Singleton,
+                         cls).__call__(
+                               *args,
+                               **kwargs
+                         )
+        return cls.__all_instances[cls]
diff --git a/src/ovirtcli/shell/engineshell.py 
b/src/ovirtcli/shell/engineshell.py
index e7ce88a..2b49a0e 100644
--- a/src/ovirtcli/shell/engineshell.py
+++ b/src/ovirtcli/shell/engineshell.py
@@ -51,8 +51,9 @@
 from ovirtcli.listeners.errorlistener import ErrorListener
 from ovirtcli.settings import OvirtCliSettings
 from ovirtcli.prompt import PromptMode
-from ovirtcli.listeners.exitlistener import ExitListener
 from cli.error import CommandError
+
+from ovirtcli.state.statemachine import StateMachine
 
 class EngineShell(cmd.Cmd, ConnectCmdShell, ActionCmdShell, \
                   ShowCmdShell, ListCmdShell, UpdateCmdShell, \
@@ -82,11 +83,11 @@
         SummaryCmdShell.__init__(self, context, parser)
         CapabilitiesCmdShell.__init__(self, context, parser)
 
-        self.onError = Event()
-        self.onInit = Event()
-        self.onExit = Event()
-        self.onPromptChange = Event()
-        self.onSigInt = Event()
+        self.onError = Event()  # triggered when error occurs
+        self.onInit = Event()  # triggered on init()
+        self.onExit = Event()  # triggered on exit
+        self.onPromptChange = Event()  # triggered onPromptChange
+        self.onSigInt = Event()  # triggered on SigInt fault
 
         self.__last_output = ''
         self.__input_buffer = ''
@@ -94,6 +95,7 @@
         self.__last_status = -1
 
         self.__register_sys_listeners()
+        self.__register_dfsm_callbacks()
         self.__init_promt()
 
         cmd.Cmd.doc_header = self.context.settings.get('ovirt-shell:commands')
@@ -182,9 +184,43 @@
 
     ########################### SYSTEM #################################
 
+    def __register_dfsm_callbacks(self):
+        """
+        registers StateMachine events callbacks
+        """
+        StateMachine.add_callback("disconnected", 
self.__on_disconnected_callback)
+        StateMachine.add_callback("connected", self.__on_connected_callback)
+        StateMachine.add_callback("exiting", self.__on_exiting_callback)
+
+    def __on_connected_callback(self, **kwargs):
+        """
+        triggered when StateMachine.CONNECTED state is acquired
+        """
+        self.context.history.enable()
+        self._print(
+           OvirtCliSettings.CONNECTED_TEMPLATE % \
+           self.context.settings.get('ovirt-shell:version')
+        )
+
+    def __on_disconnected_callback(self, **kwargs):
+        """
+        triggered when StateMachine.DISCONNECTED state is acquired
+        """
+        self.context.history.disable()
+        self._print(OvirtCliSettings.DISCONNECTED_TEMPLATE)
+        self.context.connection = None
+
+    def __on_exiting_callback(self, **kwargs):
+        """
+        triggered when StateMachine.EXITING state is acquired
+        """
+        if self.context.connection:
+            self.onecmd(
+                DisconnectCmdShell.NAME + "\n"
+            )
+
     def __register_sys_listeners(self):
         self.onError += ErrorListener(self)
-        self.onExit += ExitListener(self)
 
     def __init_promt(self):
         self._set_prompt(mode=PromptMode.Disconnected)
@@ -422,8 +458,8 @@
 
         Ctrl+D
         """
-        self._print("")
         self.onExit.fire()
+        StateMachine.exiting()  # @UndefinedVariable
         return True
 
     def do_exit(self, args):
@@ -441,6 +477,7 @@
         exit
         """
         self.onExit.fire()
+        StateMachine.exiting()  # @UndefinedVariable
         sys.exit(0)
 
     def do_help(self, args):
diff --git a/src/ovirtcli/state/__init__.py b/src/ovirtcli/state/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/ovirtcli/state/__init__.py
diff --git a/src/ovirtcli/state/dfsaevent.py b/src/ovirtcli/state/dfsaevent.py
new file mode 100644
index 0000000..79450fb
--- /dev/null
+++ b/src/ovirtcli/state/dfsaevent.py
@@ -0,0 +1,91 @@
+#
+# Copyright (c) 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#           http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+from ovirtcli.state.dfsastate import DFSAState
+from ovirtcli.annotations.requires import Requires
+import types
+
+class DFSAEvent(object):
+    '''
+    Finite-State Automata event
+    '''
+
+    def __init__(self, name, sources, destination, callbacks=[]):
+        '''
+        @param name: the state event name
+        @param sources: the source states from which destination state is 
eligible
+        @param destination: the destination state
+        @param callbacks: collection of callbacks to invoke on this event 
(TODO)
+        '''
+        self.__id = id(self)
+        self.__name = name
+        self.__sources = sources
+        self.__destination = destination
+        self.__callbacks = callbacks
+
+    def get_name(self):
+        """
+        @return: the name of DFSAEvent
+        """
+        return self.__name
+
+    def get_sources(self):
+        """
+        @return: the sources of DFSAEvent
+        """
+        return self.__sources
+
+    def get_destination(self):
+        """
+        @return: the destination of DFSAEvent
+        """
+        return self.__destination
+
+    def get_callbacks(self):
+        """
+        @return: the destination of DFSAEvent
+        """
+        return self.__callbacks
+
+    @Requires(types.MethodType)
+    def add_callback(self, callback):
+        """
+        adds new callback to event
+        
+        @param callback: the method to register
+        """
+        if self.__callbacks == None:
+            self.__callbacks = []
+        self.__callbacks.append(callback)
+
+    def __str__(self):
+        return (
+            "DFSAEvent: %s\n" + \
+            "name: %s\n" + \
+            "sources: %s\n" + \
+            "destination: %s\n" + \
+            "callbacks: %s") % (
+                 str(self.__id),
+                 self.get_name(),
+                 str([
+                      str(DFSAState(src))
+                      for src in self.get_sources()
+                    ]
+                 ),
+                 str(DFSAState(self.get_destination())),
+                 str([str(callback) for callback in self.get_callbacks()])
+             )
diff --git a/src/ovirtcli/state/dfsaeventcontext.py 
b/src/ovirtcli/state/dfsaeventcontext.py
new file mode 100644
index 0000000..1185ffb
--- /dev/null
+++ b/src/ovirtcli/state/dfsaeventcontext.py
@@ -0,0 +1,57 @@
+#
+# Copyright (c) 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#           http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class DFSAEventContext(object):
+    '''
+    Deterministic Finite-State Automata event context
+    '''
+
+
+    def __init__(self, name, sources, destination, callbacks=[]):
+        '''
+        @param name: the state event name
+        @param sources: the source states from which destination state is 
eligible
+        @param destination: the destination state
+        @param callbacks: collection of callbacks to invoke on this event 
(TODO)
+        '''
+        self.__id = id(self)
+        self.__name = name
+        self.__source = sources
+        self.__destination = destination
+
+    def get_name(self):
+        return self.__name
+
+    def get_sources(self):
+        return self.__sources
+
+    def get_destination(self):
+        return self.__destination
+
+    def get_callbacks(self):
+        return self.__callbacks
+
+    def __str__(self):
+        print 'DFSAEventContext: %s\n' + \
+              'name: %s\n' + \
+              'sources: %s\n' + \
+              'destination: %s' % \
+              (
+               str(self.__id),
+               self.get_name(),
+               self.get_sources(),
+               self.get_destination()
+              )
diff --git a/src/ovirtcli/state/dfsastate.py b/src/ovirtcli/state/dfsastate.py
new file mode 100644
index 0000000..db3fabd
--- /dev/null
+++ b/src/ovirtcli/state/dfsastate.py
@@ -0,0 +1,40 @@
+#
+# Copyright (c) 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#           http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+class DFSAState(object):
+    Disconnecting, Disconnected, Connecting, \
+    Connected, Unauthorized, Exiting = range(6)
+
+    def __init__(self, Type):
+        self.value = Type
+
+    def __str__(self):
+        if self.value == DFSAState.Disconnected:
+            return 'Disconnected'
+        if self.value == DFSAState.Connected:
+            return 'Connected'
+        if self.value == DFSAState.Unauthorized:
+            return 'Unauthorized'
+        if self.value == DFSAState.Exiting:
+            return 'Exiting'
+        if self.value == DFSAState.Disconnecting:
+            return 'Disconnecting'
+        if self.value == DFSAState.Connecting:
+            return 'Connecting'
+
+    def __eq__(self, y):
+        return self.value == y.value
diff --git a/src/ovirtcli/state/finitestatemachine.py 
b/src/ovirtcli/state/finitestatemachine.py
new file mode 100644
index 0000000..c18b234
--- /dev/null
+++ b/src/ovirtcli/state/finitestatemachine.py
@@ -0,0 +1,340 @@
+#
+# Copyright (c) 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#           http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+from ovirtcli.state.dfsaevent import DFSAEvent
+from ovirtcli.state.dfsastate import DFSAState
+from ovirtcli.annotations.requires import Requires
+from ovirtcli.events.event import Event
+from ovirtcli.meta.singleton import Singleton
+
+from cli.error import StateError, UnknownEventError
+
+import types
+
+class FiniteStateMachine(object):
+    '''
+    The Deterministic Finite-State Automata (DFSA)
+
+    =========== state maintains logic ===========
+
+    A=(A, Q, l, q0, F)
+
+    A is alphabetic
+                                 _  
+    Q is a finite set of states |_|
+                                      _      _
+    l is a state transition function |_| -> |_|
+                             _          
+    q0 is a initial state ->|_|   
+
+                               =====
+    F is a set of final states || ||
+                               =====
+
+    L = {ab^n E A* : n >= 0}
+    
+    
+    e.g:
+    
+        --b-------------------------
+        |                          |
+       ---       =====      ----   |
+     ->|q0|-a--->||q1||-a-->|q2|<---
+        --       =====      ----
+                 |   ^      |   ^
+                 b   |     a,b  |
+                 |   |      |   |
+                 -----      -----
+
+    A = ({{a, b}, {q0,q1,q2},l,{q0}, {q1})
+
+    e.g: l=(q0,a)=q1 l=(q1,b)=q1
+
+    =========== usage ===========
+
+    from ovirtcli.state.finitestatemachine import FiniteStateMachine
+    from ovirtcli.state.dfsaevent import DFSAEvent
+    from ovirtcli.state.dfsastate import DFSAState
+
+    1. define the callback for events, with callback you can either
+       perform onEvent actions or block the StateMachine till your
+       action is accomplished.
+
+        def onConnectCallback(self, **kwargs):
+            print "connect callback with:\n%s\n\n" % kwargs['event']
+    
+        def onDisconnectCallback(self, **kwargs):
+            print "disconnect callback:\n%s\n\n" % kwargs['event']
+
+    2. define the StateMachine
+    
+    class Test(object):
+        def run(self):        
+            sm = FiniteStateMachine( # define the StateMachine
+                events=[ # define events
+                    DFSAEvent( # define StateMachine event
+                      name='disconnect', # event name
+                      sources=[ # source states from which this event is 
eligible
+                           DFSAState.Connected,
+                           DFSAState.Unauthorized
+                      ],
+                      destination=DFSAState.Disconnected, # destination event 
state
+                      callbacks=[self.onDisconnectCallback]), # callbacks to 
invoke after
+                                                              # this events is 
triggered
+                    DFSAEvent(
+                      name='connect',
+                      sources=[
+                           DFSAState.Disconnected,
+                           DFSAState.Unauthorized
+                      ],
+                      destination=DFSAState.Connected,
+                      callbacks=[self.onConnectCallback]),
+                    DFSAEvent(
+                      name='unauthorized',
+                      sources=[
+                           DFSAState.Connected
+                      ],
+                      destination=DFSAState.Unauthorized,
+                      callbacks=[]),
+                ]
+            )
+
+    3. StateMachine has own events:
+
+       - onBeforeApplyState: triggered Before ApplyState occurs
+       - onAfterApplyState : triggered After ApplyState occurs
+       - onStateChange,    : triggered on any StateChange
+       - onBeforeEvent     : triggered Before Event processed
+       - onAfterEvent      : triggered After Event processed
+       - onCanMove         : triggered when CanMove() check is invoked
+       
+       you can register to them using event patter:
+       
+       sm.onBeforeEvent += OnBeforeEventListener()
+       
+       NOTE:
+
+       - EventListener must to implement IListener interface
+       - All events are provided with 'event' argument to maintain the state
+         between the events, in some case extra data can be provided such as
+         [source, destination, result, ...] depending on event's context.
+       
+    4. trigger events on StateMachine
+
+            sm.connect() # 'connect' event
+            # print sm; print "\n"
+            sm.disconnect() 'disconnect' event
+            # print sm; print "\n"
+            sm.unauthorized() 'unauthorized' event
+            # print sm; print "\n"
+            ...
+
+    Test().run()
+    '''
+
+    __metaclass__ = Singleton
+
+#   @Requires([DFSAEvent], DFSAEvent)
+#   TODO: support multi-parameters definition ^
+    def __init__(self, events, inital_state=DFSAEvent(
+                                      name='init',
+                                      sources=[],
+                                      destination=DFSAState.Disconnected
+                               )
+        ):
+        '''
+        @param events: the list of DFSA events
+        @param inital_state: the inital state of DFSA (optional)
+        '''
+
+        assert events != None
+
+        self.__id = id(self)
+        self.__current_state_obj = None
+        self.__current_state = None
+        self.__events = {}  # future use
+
+        self.onBeforeApplyState = Event()
+        self.onAfterApplyState = Event()
+        self.onStateChange = Event()
+
+        self.onBeforeEvent = Event()
+        self.onAfterEvent = Event()
+
+        self.onCanMove = Event()
+
+        self.__register_events(events)
+        self.__apply_state(inital_state)
+
+    @Requires(DFSAEvent)
+    def __apply_state(self, event):
+        """
+        applying state
+        
+        @raise StateError: when event.destination state is
+                           not applicable from the current_state
+        """
+        if self.can_move(event):
+            self.onBeforeApplyState.fire(
+                     event=event,
+                     source=self.get_current_state(),
+                     destination=event.get_destination()
+            )
+            self.onStateChange.fire(
+                     event=event,
+                     source=self.get_current_state(),
+                     destination=event.get_destination()
+            )
+
+            old_state = self.get_current_state()
+
+            self.__current_state_obj = event
+            self.__current_state = event.get_destination()
+
+            self.onAfterApplyState.fire(
+                     event=event,
+                     source=old_state,
+                     destination=self.get_current_state()
+            )
+
+        else:
+            self.__raise_state_error(event)
+
+    @Requires([DFSAEvent])
+    def __register_events(self, events):
+        """
+        registers events
+        
+        @param events: the list of events to register
+        """
+        self.__events = {}
+        for event in events:
+            self.__do_add_event(event)
+
+    @Requires(DFSAEvent)
+    def __produce_event_method(self, event):
+        """
+        produces event method
+        
+        @param eevent: event for which the method should
+                       be procured
+        """
+        def event_method(**kwargs):
+            if self.get_current_state() != event.get_destination():
+                self.onBeforeEvent.fire(event=event)
+                self.__apply_state(event)
+                if event.get_callbacks():
+                    for callback in event.get_callbacks():
+                        # TODO: consider passing all **kwargs
+                        callback(event=event)
+                self.onAfterEvent.fire(event=event)
+            else:
+                return
+        return event_method
+
+    @Requires(DFSAEvent)
+    def __raise_state_error(self, event):
+        """
+        @raise StateError: when event.destination state is
+                           not applicable from the current_state
+        """
+        raise StateError(
+                 destination=event,
+                 current=self.get_current_state()
+        )
+
+    def __raise_unknown_event(self, name):
+        """
+        @raise UnknownEventError: when is not registered
+        """
+        raise UnknownEventError(name=name)
+
+    @Requires(DFSAEvent)
+    def __do_add_event(self, event):
+        """
+        registers new event in DFSM
+        
+        @param event: event to register
+        """
+        self.__events[event.get_name()] = event
+        setattr(
+            self,
+            event.get_name(),
+            self.__produce_event_method(event)
+        )
+
+    @Requires(types.StringType)
+    def __get_event(self, name):
+        if name in self.__events.keys():
+            return self.__events[name]
+        self.__raise_unknown_event(name)
+
+    def __str__(self):
+        return 'FiniteStateMachine: %s, current state: %s' % (
+               str(self.__id),
+               DFSAState(self.get_current_state())
+        )
+
+    @Requires(DFSAEvent)
+    def add_event(self, event):
+        """
+        adds or overrides DFSAEvent event to/in DFSA
+        
+        @param event: the DFSAEvent to add/override
+        """
+        self.__do_add_event(event)
+
+    # @Requires(types.StringType, types.MethodType)
+    def add_callback(self, event_name, callback):
+        """
+        adds new callback to event
+        
+        @param event_name: the name of even to register
+                           callback for
+        @param callback: the method to register
+        """
+        self.__get_event(event_name) \
+            .get_callbacks() \
+            .append(callback)
+
+    def get_current_state(self):
+        """
+        @return: the current State of DFSA
+        """
+        return self.__current_state
+
+    @Requires(DFSAEvent)
+    def can_move(self, event):
+        """
+        checks if DFSA can move to the given event.destination
+
+        @param event: the destination DFSAEvent
+        """
+        if not self.__current_state_obj:
+            result = True  # can happen during init only!
+            # TODO: consider restricting this behavior
+        else:
+            result = self.__current_state in event.get_sources()
+
+        self.onCanMove.fire(
+                 event=event,
+                 source=self.get_current_state(),
+                 destination=event.get_destination(),
+                 result=result
+        )
+
+        return result
diff --git a/src/ovirtcli/state/statemachine.py 
b/src/ovirtcli/state/statemachine.py
new file mode 100644
index 0000000..83f452f
--- /dev/null
+++ b/src/ovirtcli/state/statemachine.py
@@ -0,0 +1,139 @@
+#
+# Copyright (c) 2013 Red Hat, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#           http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+from ovirtcli.state.dfsaevent import DFSAEvent
+from ovirtcli.state.dfsastate import DFSAState
+from ovirtcli.state.finitestatemachine import FiniteStateMachine
+
+"""
+    =========== define self instance ===========
+
+    this is singleton instance of FiniteStateMachine
+    that will be used across the app to maintain the
+    state. 
+
+    =========== state optimization =============
+
+    q ~  q` = {final states}, {other states}
+        o
+
+    q ~    q` <=> q~  q` and l(q,a) ~  l(q`,a)
+       k+1          k                k
+
+    ~   = ~ => ~  = ~
+     k+1   k    k
+
+    e.g:
+
+        ---     =====     =====
+      ->|1|-a-->||2||-a-->||3||----
+        ---     =====     =====   |
+         |      |   ^     |   ^   b
+         b      |_b_|     |_a_|   |
+         |        ^               |
+         |        |               |
+         v        a               |
+        ---     __|__           =====
+      ->|4|<--b-| 5 |<--------b-||6||-a--
+      | ---     -----           =====   |
+      |  |                        ^     |
+      | a,b                       |_____|
+      |__|
+
+
+       1 2 3 4 5 6
+      ------------
+    a |2 3 3 4 2 6     ~  = {1,4,5}, {2,3,6}
+    b |4 2 6 4 4 5      o
+                       ~  = {1,5}, {2,3}. {4}, {6}
+                        1
+
+        ----      ====      ====
+      ->|1,5|-a---||2||-a---||3||-b---
+        ----      ====      ====     |
+        |  ^      ^  |      ^   |    |
+        b  |      |  b      |   a    |
+        |  |      |__|      |___|    |
+        |  |                         |
+        |  |      ====               |
+        |  -----b-||6||---------------
+        V         ====
+       ---        ^   |
+       |4|-a,b-   |   a
+       ---     |  |___|
+        ^      |
+        |      |
+        |______|
+
+"""
+
+StateMachine = FiniteStateMachine(
+    events=[
+        DFSAEvent(
+          name='exiting',
+          sources=[
+               DFSAState.Connecting,
+               DFSAState.Connected,
+               DFSAState.Disconnected,
+               DFSAState.Unauthorized,
+          ],
+          destination=DFSAState.Exiting,
+          callbacks=[]),
+        DFSAEvent(
+          name='disconnecting',
+          sources=[
+               DFSAState.Connected,
+               DFSAState.Unauthorized,
+               DFSAState.Exiting
+          ],
+          destination=DFSAState.Disconnecting,
+          callbacks=[]),
+        DFSAEvent(
+          name='disconnected',
+          sources=[
+               DFSAState.Connected,
+               DFSAState.Unauthorized,
+               DFSAState.Disconnecting
+          ],
+          destination=DFSAState.Disconnected,
+          callbacks=[]),
+        DFSAEvent(
+          name='connecting',
+          sources=[
+               DFSAState.Disconnected,
+               DFSAState.Unauthorized
+          ],
+          destination=DFSAState.Connecting,
+          callbacks=[]),
+        DFSAEvent(
+          name='connected',
+          sources=[
+               DFSAState.Disconnected,
+               DFSAState.Unauthorized,
+               DFSAState.Connecting
+          ],
+          destination=DFSAState.Connected,
+          callbacks=[]),
+        DFSAEvent(
+          name='unauthorized',
+          sources=[
+               DFSAState.Connected
+          ],
+          destination=DFSAState.Unauthorized,
+          callbacks=[]),
+    ]
+)


-- 
To view, visit http://gerrit.ovirt.org/20451
To unsubscribe, visit http://gerrit.ovirt.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ic3ea3cf7fa82eb82c2323d96231de0a5e1094056
Gerrit-PatchSet: 1
Gerrit-Project: ovirt-engine-cli
Gerrit-Branch: master
Gerrit-Owner: Michael Pasternak <[email protected]>
_______________________________________________
Engine-patches mailing list
[email protected]
http://lists.ovirt.org/mailman/listinfo/engine-patches

Reply via email to