#Monkey-patch everything for gevent early on
gevent.monkey.patch_all()
-import inspect
import logging
-import sys
-from argparse import ArgumentParser
+from inspect import getargspec
+from argparse import ArgumentParser, ArgumentError
from base64 import b64encode
from os.path import abspath, basename, exists
-from sys import exit, stdout, stderr
+from sys import exit, stdout, stderr, argv
try:
from collections import OrderedDict
except ImportError:
from ordereddict import OrderedDict
-from colors import magenta, red, yellow, bold
-
-from kamaki import clients
-from .config import Config
-
-_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."""
+#from kamaki import clients
+from .errors import CLIError, CLISyntaxError, CLICmdIncompleteError, CLICmdSpecError
+from .config import Config #TO BE REMOVED
+from .utils import bold, magenta, red, yellow, print_list, print_dict
+from .command_tree import CommandTree
+from argument import _arguments, parse_known_args
+
+cmd_spec_locations = [
+ 'kamaki.cli.commands',
+ 'kamaki.commands',
+ 'kamaki.cli',
+ 'kamaki',
+ '']
+_commands = CommandTree(name='kamaki', description='A command line tool for poking clouds')
+
+#If empty, all commands are loaded, if not empty, only commands in this list
+#e.g. [store, lele, list, lolo] is good to load store_list but not list_store
+#First arg should always refer to a group
+candidate_command_terms = []
+allow_no_commands = False
+allow_all_commands = False
+allow_subclass_signatures = False
+
+def _allow_class_in_cmd_tree(cls):
+ global allow_all_commands
+ if allow_all_commands:
+ return True
+ global allow_no_commands
+ if allow_no_commands:
+ return False
+
+ term_list = cls.__name__.split('_')
+ global candidate_command_terms
+ index = 0
+ for term in candidate_command_terms:
+ try:
+ index += 1 if term_list[index] == term else 0
+ except IndexError: #Whole term list matched!
+ return True
+ if allow_subclass_signatures:
+ if index == len(candidate_command_terms) and len(term_list) > index:
+ try: #is subterm already in _commands?
+ _commands.get_command('_'.join(term_list[:index+1]))
+ except KeyError: #No, so it must be placed there
+ return True
+ return False
+
+ return True if index == len(term_list) else False
+
+def command():
+ """Class decorator that registers a class as a CLI command"""
def decorator(cls):
- grp, sep, cmd = cls.__name__.partition('_')
- if not sep:
- grp, cmd = None, cls.__name__
-
- #cls.api = api
- cls.group = group or grp
- cls.name = name or cmd
-
- short_description, sep, long_description = cls.__doc__.partition('\n')
- cls.description = short_description
- cls.long_description = long_description or short_description
-
- cls.syntax = syntax
- if cls.syntax is None:
- # Generate a syntax string based on main's arguments
- spec = inspect.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
-
- if cls.group not in _commands:
- _commands[cls.group] = OrderedDict()
- _commands[cls.group][cls.name] = cls
+ """Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
+
+ if not _allow_class_in_cmd_tree(cls):
+ return cls
+
+ 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
+
+ #store each term, one by one, first
+ _commands.add_command(cls.__name__, cls.description, cls)
return cls
return decorator
-def set_api_description(api, description):
- """Method to be called by api CLIs
- Each CLI can set more than one api descriptions"""
- GROUPS[api] = description
-
-def main():
-
- def print_groups():
- print('\nGroups:')
- for group in _commands:
- description = GROUPS.get(group, '')
- print(' ', group.ljust(12), description)
-
- def print_commands(group):
- description = GROUPS.get(group, '')
- if description:
- print('\n' + description)
-
- print('\nCommands:')
- for name, cls in _commands[group].items():
- print(' ', name.ljust(14), cls.description)
-
- def manage_logging_handlers(args):
- """This is mostly to handle logging for clients package"""
-
- def add_handler(name, level, prefix=''):
- h = logging.StreamHandler()
- fmt = logging.Formatter(prefix + '%(message)s')
- h.setFormatter(fmt)
- logger = logging.getLogger(name)
- logger.addHandler(h)
- logger.setLevel(level)
-
- if args.silent:
- add_handler('', logging.CRITICAL)
- elif args.debug:
- add_handler('requests', logging.INFO, prefix='* ')
- add_handler('clients.send', logging.DEBUG, prefix='> ')
- add_handler('clients.recv', logging.DEBUG, prefix='< ')
- elif args.verbose:
- add_handler('requests', logging.INFO, prefix='* ')
- add_handler('clients.send', logging.INFO, prefix='> ')
- add_handler('clients.recv', logging.INFO, prefix='< ')
- elif args.include:
- add_handler('clients.recv', logging.INFO)
+def _update_parser(parser, arguments):
+ for name, argument in arguments.items():
+ try:
+ argument.update_parser(parser, name)
+ except ArgumentError:
+ pass
+
+def _init_parser(exe):
+ parser = ArgumentParser(add_help=False)
+ parser.prog='%s <cmd_group> [<cmd_subbroup> ...] <cmd>'%exe
+ _update_parser(parser, _arguments)
+ return parser
+
+def _print_error_message(cli_err):
+ errmsg = unicode(cli_err) + (' (%s)'%cli_err.status if cli_err.status else ' ')
+ if cli_err.importance == 1:
+ errmsg = magenta(errmsg)
+ elif cli_err.importance == 2:
+ errmsg = yellow(errmsg)
+ elif cli_err.importance > 2:
+ errmsg = red(errmsg)
+ stdout.write(errmsg)
+ if cli_err.details is not None and len(cli_err.details) > 0:
+ print(': %s'%cli_err.details)
+ else:
+ print()
+
+def get_command_group(unparsed):
+ groups = _arguments['config'].get_groups()
+ for grp_candidate in unparsed:
+ if grp_candidate in groups:
+ unparsed.remove(grp_candidate)
+ return grp_candidate
+ return None
+
+def load_command(group, unparsed, reload_package=False):
+ global candidate_command_terms
+ candidate_command_terms = [group] + unparsed
+ pkg = load_group_package(group, reload_package)
+
+ #From all possible parsed commands, chose the first match in user string
+ final_cmd = _commands.get_command(group)
+ for term in unparsed:
+ cmd = final_cmd.get_subcmd(term)
+ if cmd is not None:
+ final_cmd = cmd
+ unparsed.remove(cmd.name)
+ return final_cmd
+
+def shallow_load():
+ """Load only group names and descriptions"""
+ global allow_no_commands
+ allow_no_commands = True#load only descriptions
+ for grp in _arguments['config'].get_groups():
+ load_group_package(grp)
+ allow_no_commands = False
+
+def load_group_package(group, reload_package=False):
+ spec_pkg = _arguments['config'].value.get(group, 'cli')
+ for location in cmd_spec_locations:
+ location += spec_pkg if location == '' else ('.'+spec_pkg)
+ try:
+ package = __import__(location, fromlist=['API_DESCRIPTION'])
+ except ImportError:
+ continue
+ if reload_package:
+ reload(package)
+ for grp, descr in package.API_DESCRIPTION.items():
+ _commands.add_command(grp, descr)
+ return package
+ raise CLICmdSpecError(details='Cmd Spec Package %s load failed'%spec_pkg)
+
+def print_commands(prefix=None, full_depth=False):
+ cmd_list = _commands.get_groups() if prefix is None else _commands.get_subcommands(prefix)
+ cmds = {}
+ for subcmd in cmd_list:
+ if subcmd.sublen() > 0:
+ sublen_str = '( %s more terms ... )'%subcmd.sublen()
+ cmds[subcmd.name] = [subcmd.help, sublen_str] if subcmd.has_description else subcmd_str
else:
- add_handler('', logging.WARNING)
-
- def load_groups(config):
- """load groups and import CLIs and Modules"""
- loaded_modules = {}
- for api in config.apis():
- api_cli = config.get(api, 'cli')
- if None == api_cli or len(api_cli)==0:
- print('Warnig: No Command Line Interface "%s" given for API "%s"'%(api_cli, api))
- print('\t(cli option in config file)')
- continue
- if not loaded_modules.has_key(api_cli):
- loaded_modules[api_cli] = False
- for location in CLI_LOCATIONS:
- location += api_cli if location == '' else '.%s'%api_cli
- try:
- __import__(location)
- loaded_modules[api_cli] = True
- break
- except ImportError:
- pass
- if not loaded_modules[api_cli]:
- print('Warning: failed to load Command Line Interface "%s" for API "%s"'%(api_cli, api))
- print('\t(No suitable cli in known paths)')
- continue
- if not GROUPS.has_key(api):
- GROUPS[api] = 'No description (interface: %s)'%api_cli
-
- def init_parser(exe):
- parser = ArgumentParser(add_help=False)
- parser.prog = '%s <group> <command>' % exe
- parser.add_argument('-h', '--help', dest='help', action='store_true',
- default=False,
- help="Show this help message and exit")
- parser.add_argument('--config', dest='config', metavar='PATH',
- help="Specify the path to the configuration file")
- parser.add_argument('-d', '--debug', dest='debug', action='store_true',
- default=False,
- help="Include debug output")
- parser.add_argument('-i', '--include', dest='include', action='store_true',
- default=False,
- help="Include protocol headers in the output")
- parser.add_argument('-s', '--silent', dest='silent', action='store_true',
- default=False,
- help="Silent mode, don't output anything")
- parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
- default=False,
- help="Make the operation more talkative")
- parser.add_argument('-V', '--version', dest='version', action='store_true',
- default=False,
- help="Show version number and quit")
- parser.add_argument('-o', dest='options', action='append',
- default=[], metavar="KEY=VAL",
- help="Override a config value")
- return parser
-
- def find_term_in_args(arg_list, term_list):
- """find an arg_list term in term_list. All other terms up to found
- term are rearanged at the end of arg_list, preserving relative order
- """
- arg_tail = []
- while len(arg_list) > 0:
- group = arg_list.pop(0)
- if group not in term_list:
- arg_tail.append(group)
- else:
- arg_list += arg_tail
- return group
- return None
-
- """Main Code"""
- exe = basename(sys.argv[0])
- parser = init_parser(exe)
- args, argv = parser.parse_known_args()
-
- #print version
- if args.version:
- import kamaki
- print("kamaki %s" % kamaki.__version__)
- exit(0)
-
- config = Config(args.config) if args.config else Config()
-
- #load config options from command line
- for option in args.options:
- keypath, sep, val = option.partition('=')
- if not sep:
- print("Invalid option '%s'" % option)
- exit(1)
- section, sep, key = keypath.partition('.')
- if not sep:
- print("Invalid option '%s'" % option)
- exit(1)
- config.override(section.strip(), key.strip(), val.strip())
-
- load_groups(config)
- group = find_term_in_args(argv, _commands)
- if not group:
- parser.print_help()
- print_groups()
- exit(0)
-
- parser.prog = '%s %s <command>' % (exe, group)
- command = find_term_in_args(argv, _commands[group])
-
- if not command:
- parser.print_help()
- print_commands(group)
- exit(0)
-
- cmd = _commands[group][command]()
-
- parser.prog = '%s %s %s' % (exe, group, command)
- if cmd.syntax:
- parser.prog += ' %s' % cmd.syntax
- parser.description = cmd.description
- parser.epilog = ''
- if hasattr(cmd, 'update_parser'):
- cmd.update_parser(parser)
-
- #check other args
- args, argv = parser.parse_known_args()
- if group != argv[0]:
- errmsg = red('Invalid command group '+argv[0])
- print(errmsg, file=stderr)
- exit(1)
- if command != argv[1]:
- errmsg = red('Invalid command "%s" in group "%s"'%(argv[1], argv[0]))
- print(errmsg, file=stderr)
- exit(1)
-
- if args.help:
- parser.print_help()
- exit(0)
-
- manage_logging_handlers(args)
- cmd.args = args
- cmd.config = config
+ cmds[subcmd.name] = subcmd.help
+ if len(cmds) > 0:
+ print('\nOptions:')
+ print_dict(cmds, ident=12)
+ if full_depth:
+ _commands.pretty_print()
+
+def one_command():
+ _debug = False
+ _help = False
+ _verbose = False
try:
- ret = cmd.main(*argv[2:])
- exit(ret)
- except TypeError as e:
- if e.args and e.args[0].startswith('main()'):
+ exe = basename(argv[0])
+ parser = _init_parser(exe)
+ parsed, unparsed = parse_known_args(parser)
+ _debug = _arguments['debug'].value
+ _help = _arguments['help'].value
+ _verbose = _arguments['verbose'].value
+ if _arguments['version'].value:
+ exit(0)
+
+ group = get_command_group(unparsed)
+ if group is None:
parser.print_help()
- exit(1)
- else:
- raise
+ shallow_load()
+ print_commands(full_depth=_verbose)
+ exit(0)
+
+ cmd = load_command(group, unparsed)
+ if _help or not cmd.is_command:
+ if cmd.has_description:
+ parser.description = cmd.help
+ else:
+ try:
+ parser.description = _commands.get_closest_ancestor_command(cmd.path).help
+ except KeyError:
+ parser.description = ' '
+ parser.prog = '%s %s '%(exe, cmd.path.replace('_', ' '))
+ if cmd.is_command:
+ cli = cmd.get_class()
+ parser.prog += cli.syntax
+ _update_parser(parser, cli().arguments)
+ else:
+ parser.prog += '[...]'
+ parser.print_help()
+
+ #Shuuuut, we now have to load one more level just to see what is missing
+ global allow_subclass_signatures
+ allow_subclass_signatures = True
+ load_command(group, cmd.path.split('_')[1:], reload_package=True)
+
+ print_commands(cmd.path, full_depth=_verbose)
+ exit(0)
+
+ cli = cmd.get_class()
+ executable = cli(_arguments)
+ _update_parser(parser, executable.arguments)
+ parser.prog = '%s %s %s'%(exe, cmd.path.replace('_', ' '), cli.syntax)
+ parse_known_args(parser)
+ try:
+ ret = executable.main(*unparsed)
+ exit(ret)
+ except TypeError as e:
+ if e.args and e.args[0].startswith('main()'):
+ parser.print_help()
+ exit(1)
+ else:
+ raise
except CLIError as err:
- errmsg = 'CLI Error '
- errmsg += '(%s): '%err.status if err.status else ': '
- errmsg += err.message if err.message else ''
- if err.importance == 1:
- errmsg = yellow(errmsg)
- elif err.importance == 2:
- errmsg = magenta(errmsg)
- elif err.importance > 2:
- errmsg = red(errmsg)
- print(errmsg, file=stderr)
+ if _debug:
+ raise
+ _print_error_message(err)
exit(1)
-
-if __name__ == '__main__':
- main()