from colors import magenta, red, yellow, bold
from kamaki import clients
+from .errors import CLIError
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."""
#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
# 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)
+
# 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):
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):
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):
@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))
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))
{'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>
}
}
}
'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)
@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
@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:
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):
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
)