Argument object handles part of the functionality
authorStavros Sachtouris <saxtouri@admin.grnet.gr>
Tue, 25 Sep 2012 13:50:56 +0000 (16:50 +0300)
committerStavros Sachtouris <saxtouri@admin.grnet.gr>
Tue, 25 Sep 2012 13:50:56 +0000 (16:50 +0300)
+minor fixes

kamaki/cli/__init__.py
kamaki/cli/argument.py
kamaki/cli/errors.py
kamaki/cli/utils.py
kamaki/clients/compute.py
setup.py

index c24466c..063e31e 100644 (file)
@@ -56,6 +56,7 @@ except ImportError:
 from colors import magenta, red, yellow, bold
 
 from kamaki import clients
+from .errors import CLIError
 from .config import Config
 
 _commands = OrderedDict()
@@ -63,21 +64,6 @@ _commands = OrderedDict()
 GROUPS = {}
 CLI_LOCATIONS = ['kamaki.cli.commands', 'kamaki.commands', 'kamaki.cli', 'kamaki', '']
 
-class CLIError(Exception):
-    def __init__(self, message, status=0, details='', importance=0):
-        """importance is set by the raiser
-        0 is the lowest possible importance
-        Suggested values: 0, 1, 2, 3
-        """
-        super(CLIError, self).__init__(message, status, details)
-        self.message = message
-        self.status = status
-        self.details = details
-        self.importance = importance
-
-    def __unicode__(self):
-        return unicode(self.message)
-
 def command(group=None, name=None, syntax=None):
     """Class decorator that registers a class as a CLI command."""
 
index 694d748..1f8a3c1 100644 (file)
@@ -1,8 +1,8 @@
 #A. One-command CLI
-#      1. Get a command string
-#      2. Parse out some Arguments
-#              a. We need an Argument "library" for each command-level
-#              b. Handle arg errors
+#      1. Get a command string                 DONE
+#      2. Parse out some Arguments     DONE
+#              a. We need an Argument "library" for each command-level         DONE
+#              b. Handle arg errors    
 #      3. Retrieve and validate command_sequence
 #              a. For faster responses, first command can be chosen from
 #                      a prefixed list of names, loaded from the config file
@@ -16,7 +16,7 @@
 #              d. Catch syntax errors
 #      4. Instaciate object to exec
 #              a. For path ['store', 'list', 'all'] instatiate store_list_all()
-#      5. Parse out some more Arguemnts 
+#      5. Parse out some more Arguments 
 #              a. Each command path has an "Argument library" to check your args against
 #      6. Call object.main() and catch ClientErrors
 #              a. Now, there are some command-level syntax errors that we should catch
 #      4. If cmd does not support it, for the sellected path call parse out stuff
 #              as in One-command
 #      5. Instatiate, parse_out and run object like in One-command
-#      6. Run object.main() . Again, catch ClientErrors and, probably, syntax errors
\ No newline at end of file
+#      6. Run object.main() . Again, catch ClientErrors and, probably, syntax errors
+import gevent.monkey
+#Monkey-patch everything for gevent early on
+gevent.monkey.patch_all()
+
+from sys import argv, exit
+
+from inspect import getargspec
+from os.path import basename
+from argparse import ArgumentParser
+
+from .utils import CommandTree, Argument
+from .config import Config
+from .errors import CLIError, CLISyntaxError
+
+try:
+       from colors import magenta, red, yellow, bold
+except ImportError:
+       #No colours? No worries, use dummy foo instead
+       def bold(val):
+               return val
+       red = yellow = magenta = bold
+
+_commands = CommandTree()
+
+class VersionArgument(Argument):
+       @property 
+       def value(self):
+               return super(self.__class__, self).value
+       @value.setter
+       def value(self, newvalue):
+               self._value = newvalue
+               self.main()
+
+       def main(self):
+               if self.value:
+                       import kamaki
+                       print('kamaki %s'%kamaki.__version__)
+                       self._exit(0)
+
+       def _exit(self, num):
+                       pass
+
+class ConfigArgument(Argument):
+       @property 
+       def value(self):
+               return super(self.__class__, self).value
+       @value.setter
+       def value(self, config_file):
+               self._value = Config(config_file) if config_file is not None else Config()
+
+class CmdLineConfigArgument(Argument):
+       def __init__(self, config_arg, help='', parsed_name=None, default=None):
+               super(self.__class__, self).__init__(1, help, parsed_name, default)
+               self._config_arg = config_arg
+
+       @property 
+       def value(self):
+               return super(self.__class__, self).value
+       @value.setter
+       def value(self, options):
+               if options == self.default:
+                       return
+               options = [unicode(options)] if not isinstance(options, list) else options
+               for option in options:
+                       keypath, sep, val = option.partition('=')
+                       if not sep:
+                               raise CLISyntaxError(details='Missing = between key and value: -o section.key=val')
+                       section, sep, key = keypath.partition('.')
+                       if not sep:
+                               raise CLISyntaxError(details='Missing . between section and key: -o section.key=val')
+               self._config_arg.value.override(section.strip(), key.strip(), val.strip())
+
+_config_arg = ConfigArgument(1, 'Path to configuration file', '--config')
+_arguments = dict(config = _config_arg,
+       debug = Argument(0, 'Include debug output', ('-d', '--debug')),
+       include = Argument(0, 'Include protocol headers in the output', ('-i', '--include')),
+       silent = Argument(0, 'Do not output anything', ('-s', '--silent')),
+       verbose = Argument(0, 'More info at response', ('-v', '--verbose')),
+       version = VersionArgument(0, 'Print current version', ('-V', '--version')),
+       options = CmdLineConfigArgument(_config_arg, 'Override a config value', ('-o', '--options'))
+)
+
+def command():
+       """Class decorator that registers a class as a CLI command"""
+
+       def decorator(cls):
+               """Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
+               cls.description, sep, cls.long_description = cls.__doc__.partition('\n')
+
+               # Generate a syntax string based on main's arguments
+               spec = getargspec(cls.main.im_func)
+               args = spec.args[1:]
+               n = len(args) - len(spec.defaults or ())
+               required = ' '.join('<%s>' % x.replace('____', '[:').replace('___', ':').replace('__',']').\
+                       replace('_', ' ') for x in args[:n])
+               optional = ' '.join('[%s]' % x.replace('____', '[:').replace('___', ':').replace('__', ']').\
+                       replace('_', ' ') for x in args[n:])
+               cls.syntax = ' '.join(x for x in [required, optional] if x)
+               if spec.varargs:
+                       cls.syntax += ' <%s ...>' % spec.varargs
+
+               _commands.add(cls.__name__, cls)
+               return cls
+       return decorator
+
+def _init_parser(exe):
+       parser = ArgumentParser(add_help=True)
+       parser.prog='%s <cmd_group> [<cmd_subbroup> ...] <cmd>'%exe
+       for name, argument in _arguments.items():
+               argument.update_parser(parser, name)
+       return parser
+
+def parse_known_args(parser):
+       parsed, unparsed = parser.parse_known_args()
+       for name, arg in _arguments.items():
+               arg.value = getattr(parsed, name, arg.value)
+       return parsed, unparsed
+
+def one_command():
+       exe = basename(argv[0])
+       parser = _init_parser(exe)
+       parsed, unparsed = parse_known_args(parser)
+
+
+def run_one_command():
+       try:
+               one_command()
+       except CLIError as err:
+               errmsg = '%s'%unicode(err) +' (%s)'%err.status if err.status else ' '
+               font_color = yellow if err.importance <= 1 else magenta if err.importance <=2 else red
+               from sys import stdout
+               stdout.write(font_color(errmsg))
+               if err.details is not None and len(err.details) > 0:
+                       print(': %s'%err.details)
+               else:
+                       print
+               exit(1)
+
index 3f221f1..d1c4f49 100644 (file)
@@ -30,7 +30,6 @@
 # documentation are those of the authors and should not be
 # interpreted as representing official policies, either expressed
 # or implied, of GRNET S.A.
-from . import CLIError
 
 class CLIError(Exception):
     def __init__(self, message, status=0, details='', importance=0):
@@ -48,15 +47,15 @@ class CLIError(Exception):
         return unicode(self.message)
 
 class CLISyntaxError(CLIError):
-       def __init__(self, message, status=0, details=''):
+       def __init__(self, message='Syntax Error', status=10, details=''):
                super(CLISyntaxError, self).__init__(message, status, details, importance=1)
 
 class CLIUnknownCommand(CLIError):
-       def __init__(self, message, status=12, details=''):
+       def __init__(self, message='Unknown Command', status=12, details=''):
                super(CLIUnknownCommand, self).__init__(message, status, details, importance=0)
 
 class CLICmdSpecError(CLIError):
-       def __init__(self, message, status=13, details=''):
+       def __init__(self, message='Command Specification Error', status=13, details=''):
                super(CLICmdSpecError, self).__init__(message, status, details, importance=0)
 
 def raiseCLIError(err, importance = -1):
index 98c3a58..d6a8413 100644 (file)
@@ -35,21 +35,15 @@ from .errors import CLIUnknownCommand, CLISyntaxError, CLICmdSpecError
 class Argument(object):
     """An argument that can be parsed from command line or otherwise"""
 
-    def __init__(self, name, arity, help=None, parsed_name=None):
-        self.name = name
+    def __init__(self, arity, help=None, parsed_name=None, default=None):
         self.arity = int(arity)
 
         if help is not None:
             self.help = help
         if parsed_name is not None:
             self.parsed_name = parsed_name
-
-    @property 
-    def name(self):
-        return getattr(self, '_name', None)
-    @name.setter
-    def name(self, newname):
-        self._name = unicode(newname)
+        if default is not None:
+            self.default = default
 
     @property 
     def parsed_name(self):
@@ -57,8 +51,8 @@ class Argument(object):
     @parsed_name.setter
     def parsed_name(self, newname):
         self._parsed_name = getattr(self, '_parsed_name', [])
-        if isinstance(newname, list):
-            self._parsed_name += newname
+        if isinstance(newname, list) or isinstance(newname, tuple):
+            self._parsed_name += list(newname)
         else:
             self._parsed_name.append(unicode(newname))
 
@@ -94,23 +88,27 @@ class Argument(object):
     def value(self, newvalue):
         self._value = newvalue
 
-    def update_parser(self, parser):
+    def update_parser(self, parser, name):
         """Update an argument parser with this argument info"""
-        action = 'store_true' if self.arity == 0 else 'store'
-        parser.add_argument(*(self.parsed_name), dest=self.name, action=action,
+        action = 'store_true' if self.arity==0 else 'store'
+        parser.add_argument(*self.parsed_name, dest=name, action=action,
             default=self.default, help=self.help)
 
+    def main(self):
+        """Overide this method to give functionality to ur args"""
+        raise NotImplementedError
+
     @classmethod
     def test(self):
-        h = Argument('heelp', 0, help='Display a help massage', parsed_name=['--help', '-h'])
-        b = Argument('bbb', 1, help='This is a bbb', parsed_name='--bbb')
-        c = Argument('ccc', 3, help='This is a ccc', parsed_name='--ccc')
+        h = Argument(arity=0, help='Display a help massage', parsed_name=('--help', '-h'))
+        b = Argument(arity=1, help='This is a bbb', parsed_name='--bbb')
+        c = Argument(arity=2, help='This is a ccc', parsed_name='--ccc')
 
         from argparse import ArgumentParser
         parser = ArgumentParser(add_help=False)
-        h.update_parser(parser)
-        b.update_parser(parser)
-        c.update_parser(parser)
+        h.update_parser(parser, 'hee')
+        b.update_parser(parser, 'bee')
+        c.update_parser(parser, 'cee')
 
         args, argv = parser.parse_known_args()
         print('args: %s\nargv: %s'%(args, argv))
@@ -123,19 +121,20 @@ class CommandTree(object):
         {'store': {
             'list': {
                 'all': {
-                    '_spec':<store_list_all class>
+                    '_class':<store_list_all class>
                 }
             }
         }
     then add(store_list) and store_info will create this:
         {'store': {
             'list': {
-                None: <store_list class>
+                '_class': <store_list class>
                 'all': {
-                    None: <store_list_all class>
+                    '_description': 'detail list of all containers in account'
+                    '_class': <store_list_all class>
                 },
             'info': {
-                None: <store_info class>
+                '_class': <store_info class>
                 }
             }
         }
@@ -148,10 +147,8 @@ class CommandTree(object):
         'kamaki',
         '']
 
-    def __init__(self, zero_level_commands = []):
+    def __init__(self):
         self._commands = {}
-        for cmd in zero_level_commands:
-            self._commands[unicode(cmd)] = None
 
     def _get_commands_from_prefix(self, prefix):
         path = get_pathlist_from_prefix(prefix)
@@ -171,10 +168,15 @@ class CommandTree(object):
         @param prefix can be either cmd1_cmd2_... or ['cmd1', 'cmd2', ...]
         """
         next_list =  self._get_commands_from_prefix(prefix)
+        ret = next_list.keys()
+        try:
+            ret = ret.remove('_description')
+        except ValueError:
+            pass
         try:
-            return next_list.keys().remove(None)
+            return ret.remove('_class')
         except ValueError:
-            return next_list.keys()
+            return ret
 
     def is_full_command(self, command):
         """ Check if a command exists as a full/terminal command
@@ -184,20 +186,30 @@ class CommandTree(object):
         @raise CLIUnknownCommand if command is unknown to this tree
         """
         next_level = self._get_commands_from_prefix(command)
-        if None in next_level.keys():
+        if '_class' in next_level.keys():
             return True
         return False
 
     def add(self, command, cmd_class):
         """Add a command_path-->cmd_class relation to the path """
         path_list = get_pathlist_from_prefix(command)
-        d = self._commands
+        cmds = self._commands
         for cmd in path_list:
-            if not d.has_key(cmd):
-                d[cmd] = {}
-            d = d[cmd]
-        d[None] = cmd_class #make it terminal
+            if not cmds.has_key(cmd):
+                cmds[cmd] = {}
+            cmds = cmds[cmd]
+        cmds['_class'] = cmd_class #make it terminal
 
+    def set_description(self, command, description):
+        """Add a command_path-->description to the path"""
+        path_list = get_pathlist_from_prefix(command)
+        cmds = self._commands
+        for cmd in path_list:
+            try:
+                cmds = cmds[cmd]
+            except KeyError:
+                raise CLIUnknownCommand('set_description to cmd %s failed: cmd not found'%command)
+        cmds['_description'] = description
     def load_spec_package(self, spec_package):
         loaded = False
         for location in self.cmd_spec_locations:
index 4f701f0..a25d888 100644 (file)
@@ -186,7 +186,7 @@ class ComputeClient(Client):
 
     def list_flavors(self, detail=False):
         detail = 'detail' if detail else ''
-        r.self.flavors_get(command='detail')
+        r = self.flavors_get(command='detail')
         return r.json['flavors']['values']
 
     def get_flavor_details(self, flavor_id):
index 22094cb..6ef5d60 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -54,7 +54,7 @@ setup(
     packages=['kamaki', 'kamaki.cli', 'kamaki.clients', 'kamaki.clients.connection', 'kamaki.cli.commands'],
     include_package_data=True,
     entry_points={
-        'console_scripts': ['kamaki = kamaki.cli:main']
+        'console_scripts': ['kamaki = kamaki.cli:main', 'newmaki = kamaki.cli.argument:run_one_command']
     },
     install_requires=required
 )