Revision dfee2caf

b/kamaki/cli/__init__.py
39 39
#Monkey-patch everything for gevent early on
40 40
gevent.monkey.patch_all()
41 41

  
42
import inspect
43 42
import logging
44
import sys
45 43

  
44
from inspect import getargspec
46 45
from argparse import ArgumentParser
47 46
from base64 import b64encode
48 47
from os.path import abspath, basename, exists
49
from sys import exit, stdout, stderr
48
from sys import exit, stdout, stderr, argv
50 49

  
51 50
try:
52 51
    from collections import OrderedDict
53 52
except ImportError:
54 53
    from ordereddict import OrderedDict
55 54

  
56
from colors import magenta, red, yellow, bold
57

  
58
from kamaki import clients
55
#from kamaki import clients
59 56
from .errors import CLIError
60
from .config import Config
57
from .config import Config #TO BE REMOVED
58
from .utils import magenta, red, yellow, CommandTree
59
from argument import _arguments, parse_known_args
61 60

  
62
_commands = OrderedDict()
61
_commands = CommandTree()
63 62

  
64
GROUPS = {}
63
GROUPS={}
65 64
CLI_LOCATIONS = ['kamaki.cli.commands', 'kamaki.commands', 'kamaki.cli', 'kamaki', '']
66 65

  
67
def command(group=None, name=None, syntax=None):
68
    """Class decorator that registers a class as a CLI command."""
66
def command():
67
    """Class decorator that registers a class as a CLI command"""
69 68

  
70 69
    def decorator(cls):
71
        grp, sep, cmd = cls.__name__.partition('_')
72
        if not sep:
73
            grp, cmd = None, cls.__name__
74

  
75
        #cls.api = api
76
        cls.group = group or grp
77
        cls.name = name or cmd
78

  
79
        short_description, sep, long_description = cls.__doc__.partition('\n')
80
        cls.description = short_description
81
        cls.long_description = long_description or short_description
82

  
83
        cls.syntax = syntax
84
        if cls.syntax is None:
85
            # Generate a syntax string based on main's arguments
86
            spec = inspect.getargspec(cls.main.im_func)
87
            args = spec.args[1:]
88
            n = len(args) - len(spec.defaults or ())
89
            required = ' '.join('<%s>' % x.replace('____', '[:').replace('___', ':').replace('__',']').replace('_', ' ') for x in args[:n])
90
            optional = ' '.join('[%s]' % x.replace('____', '[:').replace('___', ':').replace('__', ']').replace('_', ' ') for x in args[n:])
91
            cls.syntax = ' '.join(x for x in [required, optional] if x)
92
            if spec.varargs:
93
                cls.syntax += ' <%s ...>' % spec.varargs
94

  
95
        if cls.group not in _commands:
96
            _commands[cls.group] = OrderedDict()
97
        _commands[cls.group][cls.name] = cls
70
        """Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
71
        cls.description, sep, cls.long_description = cls.__doc__.partition('\n')
72

  
73
        # Generate a syntax string based on main's arguments
74
        spec = getargspec(cls.main.im_func)
75
        args = spec.args[1:]
76
        n = len(args) - len(spec.defaults or ())
77
        required = ' '.join('<%s>' % x.replace('____', '[:').replace('___', ':').replace('__',']').\
78
            replace('_', ' ') for x in args[:n])
79
        optional = ' '.join('[%s]' % x.replace('____', '[:').replace('___', ':').replace('__', ']').\
80
            replace('_', ' ') for x in args[n:])
81
        cls.syntax = ' '.join(x for x in [required, optional] if x)
82
        if spec.varargs:
83
            cls.syntax += ' <%s ...>' % spec.varargs
84

  
85
        _commands.add(cls.__name__, cls)
98 86
        return cls
99 87
    return decorator
100 88

  
......
103 91
    Each CLI can set more than one api descriptions"""
104 92
    GROUPS[api] = description
105 93

  
106
def main():
107

  
108
    def print_groups():
109
        print('\nGroups:')
110
        for group in _commands:
111
            description = GROUPS.get(group, '')
112
            print(' ', group.ljust(12), description)
113

  
114
    def print_commands(group):
115
        description = GROUPS.get(group, '')
116
        if description:
117
            print('\n' + description)
118

  
119
        print('\nCommands:')
120
        for name, cls in _commands[group].items():
121
            print(' ', name.ljust(14), cls.description)
122

  
123
    def manage_logging_handlers(args):
124
        """This is mostly to handle logging for clients package"""
125

  
126
        def add_handler(name, level, prefix=''):
127
            h = logging.StreamHandler()
128
            fmt = logging.Formatter(prefix + '%(message)s')
129
            h.setFormatter(fmt)
130
            logger = logging.getLogger(name)
131
            logger.addHandler(h)
132
            logger.setLevel(level)
133

  
134
        if args.silent:
135
            add_handler('', logging.CRITICAL)
136
        elif args.debug:
137
            add_handler('requests', logging.INFO, prefix='* ')
138
            add_handler('clients.send', logging.DEBUG, prefix='> ')
139
            add_handler('clients.recv', logging.DEBUG, prefix='< ')
140
        elif args.verbose:
141
            add_handler('requests', logging.INFO, prefix='* ')
142
            add_handler('clients.send', logging.INFO, prefix='> ')
143
            add_handler('clients.recv', logging.INFO, prefix='< ')
144
        elif args.include:
145
            add_handler('clients.recv', logging.INFO)
146
        else:
147
            add_handler('', logging.WARNING)
148

  
149
    def load_groups(config):
150
        """load groups and import CLIs and Modules"""
151
        loaded_modules = {}
152
        for api in config.apis():
153
            api_cli = config.get(api, 'cli')
154
            if None == api_cli or len(api_cli)==0:
155
                print('Warnig: No Command Line Interface "%s" given for API "%s"'%(api_cli, api))
156
                print('\t(cli option in config file)')
157
                continue
158
            if not loaded_modules.has_key(api_cli):
159
                loaded_modules[api_cli] = False
160
                for location in CLI_LOCATIONS:
161
                    location += api_cli if location == '' else '.%s'%api_cli
162
                    try:
163
                        __import__(location)
164
                        loaded_modules[api_cli] = True
165
                        break
166
                    except ImportError:
167
                        pass
168
                if not loaded_modules[api_cli]:
169
                    print('Warning: failed to load Command Line Interface "%s" for API "%s"'%(api_cli, api))
170
                    print('\t(No suitable cli in known paths)')
171
                    continue
172
            if not GROUPS.has_key(api):
173
                GROUPS[api] = 'No description (interface: %s)'%api_cli
174

  
175
    def init_parser(exe):
176
        parser = ArgumentParser(add_help=False)
177
        parser.prog = '%s <group> <command>' % exe
178
        parser.add_argument('-h', '--help', dest='help', action='store_true',
179
                          default=False,
180
                          help="Show this help message and exit")
181
        parser.add_argument('--config', dest='config', metavar='PATH',
182
                          help="Specify the path to the configuration file")
183
        parser.add_argument('-d', '--debug', dest='debug', action='store_true',
184
                          default=False,
185
                          help="Include debug output")
186
        parser.add_argument('-i', '--include', dest='include', action='store_true',
187
                          default=False,
188
                          help="Include protocol headers in the output")
189
        parser.add_argument('-s', '--silent', dest='silent', action='store_true',
190
                          default=False,
191
                          help="Silent mode, don't output anything")
192
        parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
193
                          default=False,
194
                          help="Make the operation more talkative")
195
        parser.add_argument('-V', '--version', dest='version', action='store_true',
196
                          default=False,
197
                          help="Show version number and quit")
198
        parser.add_argument('-o', dest='options', action='append',
199
                          default=[], metavar="KEY=VAL",
200
                          help="Override a config value")
201
        return parser
202

  
203
    def find_term_in_args(arg_list, term_list):
204
        """find an arg_list term in term_list. All other terms up to found
205
        term are rearanged at the end of arg_list, preserving relative order
206
        """
207
        arg_tail = []
208
        while len(arg_list) > 0:
209
            group = arg_list.pop(0)
210
            if group not in term_list:
211
                arg_tail.append(group)
212
            else:
213
                arg_list += arg_tail
214
                return group
215
        return None
216

  
217
    """Main Code"""
218
    exe = basename(sys.argv[0])
219
    parser = init_parser(exe)
220
    args, argv = parser.parse_known_args()
221

  
222
    #print version
223
    if args.version:
224
        import kamaki
225
        print("kamaki %s" % kamaki.__version__)
226
        exit(0)
227

  
228
    config = Config(args.config) if args.config else Config()
229

  
230
    #load config options from command line
231
    for option in args.options:
232
        keypath, sep, val = option.partition('=')
233
        if not sep:
234
            print("Invalid option '%s'" % option)
235
            exit(1)
236
        section, sep, key = keypath.partition('.')
237
        if not sep:
238
            print("Invalid option '%s'" % option)
239
            exit(1)
240
        config.override(section.strip(), key.strip(), val.strip())
241

  
242
    load_groups(config)
243
    group = find_term_in_args(argv, _commands)
244
    if not group:
245
        parser.print_help()
246
        print_groups()
247
        exit(0)
248

  
249
    parser.prog = '%s %s <command>' % (exe, group)
250
    command = find_term_in_args(argv, _commands[group])
251

  
252
    if not command:
253
        parser.print_help()
254
        print_commands(group)
255
        exit(0)
256

  
257
    cmd = _commands[group][command]()
258

  
259
    parser.prog = '%s %s %s' % (exe, group, command)
260
    if cmd.syntax:
261
        parser.prog += '  %s' % cmd.syntax
262
    parser.description = cmd.description
263
    parser.epilog = ''
264
    if hasattr(cmd, 'update_parser'):
265
        cmd.update_parser(parser)
266

  
267
    #check other args
268
    args, argv = parser.parse_known_args()
269
    if group != argv[0]:
270
        errmsg = red('Invalid command group '+argv[0])
271
        print(errmsg, file=stderr)
272
        exit(1)
273
    if command != argv[1]:
274
        errmsg = red('Invalid command "%s" in group "%s"'%(argv[1], argv[0]))
275
        print(errmsg, file=stderr)
276
        exit(1)
277

  
278
    if args.help:
279
        parser.print_help()
280
        exit(0)
281

  
282
    manage_logging_handlers(args)
283
    cmd.args = args
284
    cmd.config = config
94
def _init_parser(exe):
95
    parser = ArgumentParser(add_help=True)
96
    parser.prog='%s <cmd_group> [<cmd_subbroup> ...] <cmd>'%exe
97
    for name, argument in _arguments.items():
98
        argument.update_parser(parser, name)
99
    return parser
100

  
101
def _print_error_message(cli_err):
102
    errmsg = '%s'%unicode(cli_err) +' (%s)'%cli_err.status if cli_err.status else ' '
103
    if cli_err.importance == 1:
104
        errmsg = magenta(errmsg)
105
    elif cli_err.importance == 2:
106
        errmsg = yellow(errmsg)
107
    elif cli_err.importance > 2:
108
        errmsg = red(errmsg)
109
    stdout.write(errmsg)
110
    if cli_err.details is not None and len(cli_err.details) > 0:
111
        print(': %s'%cli_err.details)
112
    else:
113
        print
114

  
115
def one_command():
285 116
    try:
286
        ret = cmd.main(*argv[2:])
287
        exit(ret)
288
    except TypeError as e:
289
        if e.args and e.args[0].startswith('main()'):
290
            parser.print_help()
291
            exit(1)
292
        else:
293
            raise
117
        exe = basename(argv[0])
118
        parser = _init_parser(exe)
119
        parsed, unparsed = parse_known_args(parser)
120
        if _arguments['version'].value:
121
            exit(0)
294 122
    except CLIError as err:
295
        errmsg = 'CLI Error '
296
        errmsg += '(%s): '%err.status if err.status else ': '
297
        errmsg += unicode(err.message) if err.message else ''
298
        if err.importance == 1:
299
            errmsg = yellow(errmsg)
300
        elif err.importance == 2:
301
            errmsg = magenta(errmsg)
302
        elif err.importance > 2:
303
            errmsg = red(errmsg)
304
        print(errmsg, file=stderr)
123
        _print_error_message(err)
305 124
        exit(1)
306

  
307
if __name__ == '__main__':
308
    main()
b/kamaki/cli/argument.py
34 34
#Monkey-patch everything for gevent early on
35 35
gevent.monkey.patch_all()
36 36

  
37
from sys import argv, exit
37
from sys import exit
38 38

  
39
from inspect import getargspec
40
from os.path import basename
41
from argparse import ArgumentParser
42

  
43
from .utils import CommandTree, Argument
44 39
from .config import Config
45
from .errors import CLIError, CLISyntaxError
46

  
47
try:
48
	from colors import magenta, red, yellow, bold
49
except ImportError:
50
	#No colours? No worries, use dummy foo instead
51
	def bold(val):
52
		return val
53
	red = yellow = magenta = bold
54

  
55
_commands = CommandTree()
40
from .errors import CLISyntaxError
41

  
42
class Argument(object):
43
    """An argument that can be parsed from command line or otherwise"""
44

  
45
    def __init__(self, arity, help=None, parsed_name=None, default=None):
46
        self.arity = int(arity)
47

  
48
        if help is not None:
49
            self.help = help
50
        if parsed_name is not None:
51
            self.parsed_name = parsed_name
52
        if default is not None:
53
            self.default = default
54

  
55
    @property 
56
    def parsed_name(self):
57
        return getattr(self, '_parsed_name', None)
58
    @parsed_name.setter
59
    def parsed_name(self, newname):
60
        self._parsed_name = getattr(self, '_parsed_name', [])
61
        if isinstance(newname, list) or isinstance(newname, tuple):
62
            self._parsed_name += list(newname)
63
        else:
64
            self._parsed_name.append(unicode(newname))
65

  
66
    @property 
67
    def help(self):
68
        return getattr(self, '_help', None)
69
    @help.setter
70
    def help(self, newhelp):
71
        self._help = unicode(newhelp)
72

  
73
    @property 
74
    def arity(self):
75
        return getattr(self, '_arity', None)
76
    @arity.setter
77
    def arity(self, newarity):
78
        newarity = int(newarity)
79
        assert newarity >= 0
80
        self._arity = newarity
81

  
82
    @property 
83
    def default(self):
84
        if not hasattr(self, '_default'):
85
            self._default = False if self.arity == 0 else None
86
        return self._default
87
    @default.setter
88
    def default(self, newdefault):
89
        self._default = newdefault
90

  
91
    @property 
92
    def value(self):
93
        return getattr(self, '_value', self.default)
94
    @value.setter
95
    def value(self, newvalue):
96
        self._value = newvalue
97

  
98
    def update_parser(self, parser, name):
99
        """Update an argument parser with this argument info"""
100
        action = 'store_true' if self.arity==0 else 'store'
101
        parser.add_argument(*self.parsed_name, dest=name, action=action,
102
            default=self.default, help=self.help)
103

  
104
    def main(self):
105
        """Overide this method to give functionality to ur args"""
106
        raise NotImplementedError
107

  
108
    @classmethod
109
    def test(self):
110
        h = Argument(arity=0, help='Display a help massage', parsed_name=('--help', '-h'))
111
        b = Argument(arity=1, help='This is a bbb', parsed_name='--bbb')
112
        c = Argument(arity=2, help='This is a ccc', parsed_name='--ccc')
113

  
114
        from argparse import ArgumentParser
115
        parser = ArgumentParser(add_help=False)
116
        h.update_parser(parser, 'hee')
117
        b.update_parser(parser, 'bee')
118
        c.update_parser(parser, 'cee')
119

  
120
        args, argv = parser.parse_known_args()
121
        print('args: %s\nargv: %s'%(args, argv))
56 122

  
57 123
class VersionArgument(Argument):
58 124
	@property 
......
67 133
		if self.value:
68 134
			import kamaki
69 135
			print('kamaki %s'%kamaki.__version__)
70
			self._exit(0)
71

  
72
	def _exit(self, num):
73
			pass
74 136

  
75 137
class ConfigArgument(Argument):
76 138
	@property 
......
112 174
	options = CmdLineConfigArgument(_config_arg, 'Override a config value', ('-o', '--options'))
113 175
)
114 176

  
115
def command():
116
	"""Class decorator that registers a class as a CLI command"""
117

  
118
	def decorator(cls):
119
		"""Any class with name of the form cmd1_cmd2_cmd3_... is accepted"""
120
		cls.description, sep, cls.long_description = cls.__doc__.partition('\n')
121

  
122
		# Generate a syntax string based on main's arguments
123
		spec = getargspec(cls.main.im_func)
124
		args = spec.args[1:]
125
		n = len(args) - len(spec.defaults or ())
126
		required = ' '.join('<%s>' % x.replace('____', '[:').replace('___', ':').replace('__',']').\
127
			replace('_', ' ') for x in args[:n])
128
		optional = ' '.join('[%s]' % x.replace('____', '[:').replace('___', ':').replace('__', ']').\
129
			replace('_', ' ') for x in args[n:])
130
		cls.syntax = ' '.join(x for x in [required, optional] if x)
131
		if spec.varargs:
132
			cls.syntax += ' <%s ...>' % spec.varargs
133

  
134
		_commands.add(cls.__name__, cls)
135
		return cls
136
	return decorator
137

  
138
def _init_parser(exe):
139
	parser = ArgumentParser(add_help=True)
140
	parser.prog='%s <cmd_group> [<cmd_subbroup> ...] <cmd>'%exe
141
	for name, argument in _arguments.items():
142
		argument.update_parser(parser, name)
143
	return parser
144

  
145 177
def parse_known_args(parser):
146 178
	parsed, unparsed = parser.parse_known_args()
147 179
	for name, arg in _arguments.items():
148 180
		arg.value = getattr(parsed, name, arg.value)
149 181
	return parsed, unparsed
150 182

  
151
def one_command():
152
	exe = basename(argv[0])
153
	parser = _init_parser(exe)
154
	parsed, unparsed = parse_known_args(parser)
155

  
156

  
157
def run_one_command():
158
	try:
159
		one_command()
160
	except CLIError as err:
161
		errmsg = '%s'%unicode(err) +' (%s)'%err.status if err.status else ' '
162
		font_color = yellow if err.importance <= 1 else magenta if err.importance <=2 else red
163
		from sys import stdout
164
		stdout.write(font_color(errmsg))
165
		if err.details is not None and len(err.details) > 0:
166
			print(': %s'%err.details)
167
		else:
168
			print
169
		exit(1)
170 183

  
b/kamaki/cli/utils.py
30 30
# documentation are those of the authors and should not be
31 31
# interpreted as representing official policies, either expressed
32 32
# or implied, of GRNET S.A.
33
from .errors import CLIUnknownCommand, CLISyntaxError, CLICmdSpecError
34

  
35
class Argument(object):
36
    """An argument that can be parsed from command line or otherwise"""
37

  
38
    def __init__(self, arity, help=None, parsed_name=None, default=None):
39
        self.arity = int(arity)
40

  
41
        if help is not None:
42
            self.help = help
43
        if parsed_name is not None:
44
            self.parsed_name = parsed_name
45
        if default is not None:
46
            self.default = default
47

  
48
    @property 
49
    def parsed_name(self):
50
        return getattr(self, '_parsed_name', None)
51
    @parsed_name.setter
52
    def parsed_name(self, newname):
53
        self._parsed_name = getattr(self, '_parsed_name', [])
54
        if isinstance(newname, list) or isinstance(newname, tuple):
55
            self._parsed_name += list(newname)
56
        else:
57
            self._parsed_name.append(unicode(newname))
58

  
59
    @property 
60
    def help(self):
61
        return getattr(self, '_help', None)
62
    @help.setter
63
    def help(self, newhelp):
64
        self._help = unicode(newhelp)
65

  
66
    @property 
67
    def arity(self):
68
        return getattr(self, '_arity', None)
69
    @arity.setter
70
    def arity(self, newarity):
71
        newarity = int(newarity)
72
        assert newarity >= 0
73
        self._arity = newarity
74

  
75
    @property 
76
    def default(self):
77
        if not hasattr(self, '_default'):
78
            self._default = False if self.arity == 0 else None
79
        return self._default
80
    @default.setter
81
    def default(self, newdefault):
82
        self._default = newdefault
83

  
84
    @property 
85
    def value(self):
86
        return getattr(self, '_value', self.default)
87
    @value.setter
88
    def value(self, newvalue):
89
        self._value = newvalue
90

  
91
    def update_parser(self, parser, name):
92
        """Update an argument parser with this argument info"""
93
        action = 'store_true' if self.arity==0 else 'store'
94
        parser.add_argument(*self.parsed_name, dest=name, action=action,
95
            default=self.default, help=self.help)
96

  
97
    def main(self):
98
        """Overide this method to give functionality to ur args"""
99
        raise NotImplementedError
100

  
101
    @classmethod
102
    def test(self):
103
        h = Argument(arity=0, help='Display a help massage', parsed_name=('--help', '-h'))
104
        b = Argument(arity=1, help='This is a bbb', parsed_name='--bbb')
105
        c = Argument(arity=2, help='This is a ccc', parsed_name='--ccc')
106

  
107
        from argparse import ArgumentParser
108
        parser = ArgumentParser(add_help=False)
109
        h.update_parser(parser, 'hee')
110
        b.update_parser(parser, 'bee')
111
        c.update_parser(parser, 'cee')
112

  
113
        args, argv = parser.parse_known_args()
114
        print('args: %s\nargv: %s'%(args, argv))
33
try:
34
    from colors import magenta, red, yellow, bold
35
except ImportError:
36
    #No colours? No worries, use dummy foo instead
37
    def bold(val):
38
        return val
39
    red = yellow = magenta = bold
40

  
41
from .errors import CLIUnknownCommand, CLICmdSpecError, CLIError
42

  
43
"""
44
def magenta(val):
45
    return magenta(val)
46
def red(val):
47
    return red(val)
48
def yellow(val):
49
    return yellow(val)
50
def bold(val):
51
    return bold(val)
52
"""
115 53

  
116 54
class CommandTree(object):
117 55
    """A tree of command terms usefull for fast commands checking
b/setup.py
54 54
    packages=['kamaki', 'kamaki.cli', 'kamaki.clients', 'kamaki.clients.connection', 'kamaki.cli.commands'],
55 55
    include_package_data=True,
56 56
    entry_points={
57
        'console_scripts': ['kamaki = kamaki.cli:main', 'newmaki = kamaki.cli.argument:run_one_command']
57
        'console_scripts': ['kamaki = kamaki.cli:one_command']
58 58
    },
59 59
    install_requires=required
60 60
)

Also available in: Unified diff