Commit 0b368c8c authored by Stavros Sachtouris's avatar Stavros Sachtouris
Browse files

Refactor CommandTree, parse and get cli class

parent 017d37ce
......@@ -55,18 +55,52 @@ except ImportError:
#from kamaki import clients
from .errors import CLIError, CLISyntaxError, CLICmdIncompleteError
from .config import Config #TO BE REMOVED
from .utils import bold, magenta, red, yellow, CommandTree, print_list
from .utils import bold, magenta, red, yellow, CommandTree, print_list, print_dict
from argument import _arguments, parse_known_args
_commands = CommandTree()
cmd_spec_locations = [
'kamaki.cli.commands',
'kamaki.commands',
'kamaki.cli',
'kamaki',
'']
_commands = CommandTree(description='A command line tool for poking clouds')
#basic command groups
#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 = [None]
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"""
term_list = cls.__name__.split('_')
global candidate_command_terms
tmp_tree = _commands
if len(candidate_command_terms) > 0:
#This is the case of a one-command execution: discard if not requested
if term_list[0] != candidate_command_terms[0]:
return cls
i = 0
for term in term_list:
#check if the term is requested by used
if term not in candidate_command_terms[i:]:
return cls
i = 1+candidate_command_terms.index(term)
#now, put the term in the tree
if term not in tmp_tree.get_command_names():
tmp_tree.add_command(term)
tmp_tree = tmp_tree.get_command(term)
else:
#Just insert everything in the tree
for term in term_list:
if term not in tmp_tree.get_command_names():
tmp_tree.add_command(term)
tmp_tree = tmp_tree.get_command()
cls.description, sep, cls.long_description = cls.__doc__.partition('\n')
# Generate a syntax string based on main's arguments
......@@ -81,15 +115,11 @@ def command():
if spec.varargs:
cls.syntax += ' <%s ...>' % spec.varargs
_commands.add(cls.__name__, cls)
#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 _init_parser(exe):
parser = ArgumentParser(add_help=True)
parser.prog='%s <cmd_group> [<cmd_subbroup> ...] <cmd>'%exe
......@@ -141,20 +171,107 @@ def _retrieve_cmd(unparsed):
print_list(_commands.list(cmd_str))
return None
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 _order_in_list(list1, list2):
order = 0
for i,term in enumerate(list1):
order += len(list2)*i*list2.index(term)
return order
def load_command(group, unparsed):
global candidate_command_terms
candidate_command_terms = [group] + unparsed
pkg = load_group_package(group)
#From all possible parsed commands, chose one
final_cmd = group
next_names = [None]
next_names = _commands.get_command_names(final_cmd)
while len(next_names) > 0:
if len(next_names) == 1:
final_cmd+='_'+next_names[0]
else:#choose the first in user string
pos = unparsed.index(next_names[0])
choice = 0
for i, name in enumerate(next_names[1:]):
tmp_index = unparsed.index(name)
if tmp_index < pos:
pos = tmp_index
choice = i+1
final_cmd+='_'+next_names[choice]
next_names = _commands.get_command_names(final_cmd)
cli = _commands.get_class(final_cmd)
if cli is None:
raise CLICmdIncompleteError(details='%s'%final_cmd)
return cli
def load_group_descriptions(spec_pkg):
for location in cmd_spec_locations:
location += spec_pkg if location == '' else ('.'+spec_pkg)
try:
package = __import__(location, fromlist=['API_DESCRIPTION'])
except ImportError:
continue
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 shallow_load_groups():
"""Load only group names and descriptions"""
for grp in _arguments['config'].get_groups():
spec_pkg = _arguments['config'].value.get(grp, 'cli')
load_group_descriptions(spec_pkg)
def load_group_package(group):
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)
except ImportError:
continue
return package
raise CLICmdSpecError(details='Cmd Spec Package %s load failed'%spec_pkg)
def print_commands(prefix=[]):
grps = {}
for grp in _commands.get_command_names(prefix):
grps[grp] = _commands.get_description(grp)
print_dict(grps, ident=12)
def one_command():
_debug = False
try:
exe = basename(argv[0])
parser = _init_parser(exe)
parsed, unparsed = parse_known_args(parser)
if _arguments['debug'].value:
_debug = True
if _arguments['version'].value:
exit(0)
_commands.set_groups(_arguments['config'].get_groups())
cmd = _retrieve_cmd(unparsed)
if cmd is None:
group = get_command_group(unparsed)
if group is None:
parser.print_help()
shallow_load_groups()
print('\nCommand groups:')
print_commands()
exit(0)
cli = load_command(group, unparsed)
print('And this is how I get my command! YEAAAAAAH! %s'%cli)
except CLIError as err:
if _debug:
raise
_print_error_message(err)
exit(1)
......@@ -32,8 +32,9 @@
# or implied, of GRNET S.A.command
from kamaki.cli import command, set_api_description
set_api_description('astakos', 'Astakos API commands')
from kamaki.cli import command#, set_api_description
#set_api_description('astakos', 'Astakos API commands')
API_DESCRIPTION = {'astakos':'Astakos API commands'}
from kamaki.clients.astakos import AstakosClient, ClientError
from kamaki.cli.utils import print_dict
from kamaki.cli.errors import raiseCLIError
......
......@@ -31,8 +31,9 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from kamaki.cli import command, set_api_description
set_api_description('config', 'Configuration commands')
from kamaki.cli import command#, set_api_description
#set_api_description('config', 'Configuration commands')
API_DESCRIPTION = {'config':'Configuration commands'}
@command()
class config_list(object):
......
......@@ -31,14 +31,17 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from kamaki.cli import command, set_api_description
from kamaki.cli.utils import print_dict, print_items, print_list, format_size
from kamaki.cli import command#, set_api_description
from kamaki.cli.utils import print_dict, print_items, print_list, format_size, bold
from kamaki.cli.errors import CLIError, raiseCLIError
from colors import bold
set_api_description('server', "Compute/Cyclades API server commands")
set_api_description('flavor', "Compute/Cyclades API flavor commands")
set_api_description('image', "Compute/Cyclades or Glance API image commands")
set_api_description('network', "Compute/Cyclades API network commands")
#set_api_description('server', "Compute/Cyclades API server commands")
#set_api_description('flavor', "'Compute/Cyclades API flavor commands'")
#set_api_description('image', "Compute/Cyclades or Glance API image commands")
#set_api_description('network', "Compute/Cyclades API network commands")
API_DESCRIPTION = {'server':'Compute/Cyclades API server commands',
'flavor':'Compute/Cyclades API flavor commands',
'image':'Compute/Cyclades or Glance API image commands',
'network': 'Compute/Cyclades API network commands'}
from kamaki.clients.cyclades import CycladesClient, ClientError
class _init_cyclades(object):
......
......@@ -31,12 +31,14 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.command
from kamaki.cli import command, set_api_description
from kamaki.cli import command#, set_api_description
from kamaki.cli.errors import raiseCLIError
from kamaki.cli.utils import print_dict, print_items
set_api_description('image', "Compute/Cyclades or Glance API image commands")
#set_api_description('image', "Compute/Cyclades or Glance API image commands")
API_DESCRIPTION = {'image':'Compute/Cyclades or Glance API image commands'}
from kamaki.clients.image import ImageClient, ClientError
class _init_image(object):
def main(self):
try:
......
......@@ -31,11 +31,12 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.command
from kamaki.cli import command, set_api_description
from kamaki.cli import command#, set_api_description
from kamaki.clients.utils import filter_in
from kamaki.cli.errors import CLIError, raiseCLIError
from kamaki.cli.utils import format_size, print_dict, pretty_keys, print_list
set_api_description('store', 'Pithos+ storage commands')
#set_api_description('store', 'Pithos+ storage commands')
API_DESCRIPTION = {'store':'Pithos+ storage commands'}
from kamaki.clients.pithos import PithosClient, ClientError
from colors import bold
from sys import stdout, exit
......
......@@ -52,7 +52,7 @@ class CLISyntaxError(CLIError):
class CLIUnknownCommand(CLIError):
def __init__(self, message='Unknown Command', status=12, details=''):
super(CLIUnknownCommand, self).__init__(message, status, details, importance=0)
super(CLIUnknownCommand, self).__init__(message, status, details, importance=1)
class CLICmdSpecError(CLIError):
def __init__(self, message='Command Specification Error', status=13, details='', importance=1):
......
......@@ -40,152 +40,78 @@ except ImportError:
from .errors import CLIUnknownCommand, CLICmdIncompleteError, CLICmdSpecError, CLIError
"""
def magenta(val):
return magenta(val)
def red(val):
return red(val)
def yellow(val):
return yellow(val)
def bold(val):
return bold(val)
"""
class CommandTree(object):
"""A tree of command terms usefull for fast commands checking
None key is used to denote that its parent is a terminal symbol
and also the command spec class
e.g. add(store_list_all) will result to this:
{'store': {
'list': {
'all': {
'_class':<store_list_all class>
}
}
}
then add(store_list) and store_info will create this:
{'store': {
'list': {
'_class': <store_list class>
'all': {
'_description': 'detail list of all containers in account'
'_class': <store_list_all class>
},
'info': {
'_class': <store_info class>
}
}
}
"""
cmd_spec_locations = [
'kamaki.cli.commands',
'kamaki.commands',
'kamaki.cli',
'kamaki',
'']
def __init__(self):
self._commands = {}
def set_groups(self, groups):
for grp in groups:
self._commands[grp] = {}
def get_groups(self):
return self._commands.keys()
def _get_commands_from_prefix(self, prefix):
if len(prefix) == 0:
return self._commands
path = get_pathlist_from_prefix(prefix)
next_list = self._commands
def __init__(self, run_class=None, description='', commands={}):
self.run_class = run_class
self.description = description
self.commands = commands
def get_command_names(self, prefix=[]):
cmd = self.get_command(prefix)
return cmd.commands.keys()
def get_terminal_commands(self, prefix=''):
cmd = self.get_command(prefix)
terminal_cmds = [prefix] if cmd.is_command() else []
prefix = '' if len(prefix) == 0 else '%s_'%prefix
for term, tree in cmd.commands.items():
xtra = self.get_terminal_commands(prefix+term)
terminal_cmds.append(*xtra)
return terminal_cmds
def add_command(self, new_command, new_descr='', new_class=None):
cmd_list = new_command.split('_')
cmd = self.get_command(cmd_list[:-1])
try:
for cmd in path:
next_list = next_list[unicode(cmd)]
except TypeError, KeyError:
error_index = path.index(cmd)
details='Command %s not in path %s'%(unicode(cmd), path[:error_index])
raise CLIUnknownCommand('Unknown command', details=details)
assert isinstance(next_list,dict)
return next_list
def list(self, prefix=[]):
""" List the commands after prefix
@param prefix can be either cmd1_cmd2_... or ['cmd1', 'cmd2', ...]
existing = cmd.get_command(cmd_list[-1])
if new_class is not None:
existing.run_class = new_class
if new_descr not in (None, ''):
existing.description = new_descr
except CLIUnknownCommand:
cmd.commands[new_command] = CommandTree(new_class,new_descr,{})
def is_command(self, command=''):
if self.get_command(command).run_class is None:
return False
return True
def get_class(self, command=''):
cmd = self.get_command(command)
return cmd.run_class
def set_class(self, command, new_class):
cmd = self.get_command(command)
cmd.run_class = new_class
def get_description(self, command):
cmd = self.get_command(command)
return cmd.description
def set_description(self, command, new_descr):
cmd = self.get_command(command)
cmd.description = new_descr
def copy_command(self, prefix=''):
cmd = self.get_command(prefix)
from copy import deepcopy
return deepcopy(cmd)
def get_command(self, command):
"""
next_list = self._get_commands_from_prefix(prefix)
ret = next_list.keys()
try:
ret = ret.remove('_description')
except ValueError:
pass
try:
return ret.remove('_class')
except ValueError:
return ret
def get_class(self, command):
""" Check if a command exists as a full/terminal command
e.g. store_list is full, store is partial, stort is not existing
@param command can either be a cmd1_cmd2_... str or a ['cmd1, cmd2, ...'] list
@return True if this command is in this Command Tree, False otherwise
@raise CLIUnknownCommand if command is unknown to this tree
@return a tuple of the form (cls_object, 'description text', {term1':(...), 'term2':(...)})
"""
next_level = self._get_commands_from_prefix(command)
path = get_pathlist_from_prefix(command)
cmd = self
try:
return next_level['_class']
for term in path:
cmd = cmd.commands[term]
except KeyError:
raise CLICmdIncompleteError(details='Cmd %s is not a full cmd'%command)
def add(self, command, cmd_class):
"""Add a command_path-->cmd_class relation to the path """
path_list = get_pathlist_from_prefix(command)
cmds = self._commands
for cmd in path_list:
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(details='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:
location += spec_package if location == '' else '.%s'%spec_package
try:
__import__(location) #a class decorator will put evetyrhing in place
loaded = True
break
except ImportError:
pass
if not loaded:
raise CLICmdSpecError(details='Cmd Spec Package %s load failed'%spec_package)
def load_spec(self, spec_package, spec):
"""Load spec from a non nessecery loaded spec package"""
loaded = False
for location in self.cmd_spec_locations:
location += spec_package if location == '' else '.%s'%spec_package
try:
__import__(location, fromlist=[spec])
loaded = True
break
except ImportError:
pass
if not loaded:
raise CLICmdSpecError('Cmd Spec %s load failed'%spec)
error_index = path.index(term)
details='Command term %s not in path %s'%(unicode(term), path[:error_index])
raise CLIUnknownCommand('Unknown command', details=details)
return cmd
def get_pathlist_from_prefix(prefix):
if isinstance(prefix, list):
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment