Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument.py @ ca092af4

History | View | Annotate | Download (15.2 kB)

1
# Copyright 2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#     copyright notice, this list of conditions and the following
9
#     disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#     copyright notice, this list of conditions and the following
13
#     disclaimer in the documentation and/or other materials
14
#     provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from kamaki.cli.config import Config
35
from kamaki.cli.errors import CLISyntaxError, raiseCLIError
36
from kamaki.cli.utils import split_input
37
from logging import getLogger
38
from datetime import datetime as dtm
39

    
40

    
41
from argparse import ArgumentParser, ArgumentError
42
from argparse import RawDescriptionHelpFormatter
43

    
44
try:
45
    from progress.bar import ShadyBar as KamakiProgressBar
46
except ImportError:
47
    try:
48
        from progress.bar import Bar as KamakiProgressBar
49
    except ImportError:
50
        pass
51
    # progress not installed - pls, pip install progress
52
    pass
53

    
54
kloger = getLogger('kamaki')
55

    
56

    
57
class Argument(object):
58
    """An argument that can be parsed from command line or otherwise.
59
    This is the general Argument class. It is suggested to extent this
60
    class into more specific argument types.
61
    """
62

    
63
    def __init__(self, arity, help=None, parsed_name=None, default=None):
64
        self.arity = int(arity)
65

    
66
        if help is not None:
67
            self.help = help
68
        if parsed_name is not None:
69
            self.parsed_name = parsed_name
70
        if default is not None:
71
            self.default = default
72

    
73
    @property
74
    def parsed_name(self):
75
        """the string which will be recognised by the parser as an instance
76
            of this argument
77
        """
78
        return getattr(self, '_parsed_name', None)
79

    
80
    @parsed_name.setter
81
    def parsed_name(self, newname):
82
        self._parsed_name = getattr(self, '_parsed_name', [])
83
        if isinstance(newname, list) or isinstance(newname, tuple):
84
            self._parsed_name += list(newname)
85
        else:
86
            self._parsed_name.append(unicode(newname))
87

    
88
    @property
89
    def help(self):
90
        """a user friendly help message"""
91
        return getattr(self, '_help', None)
92

    
93
    @help.setter
94
    def help(self, newhelp):
95
        self._help = unicode(newhelp)
96

    
97
    @property
98
    def arity(self):
99
        """negative for repeating, 0 for flag, 1 or more for values"""
100
        return getattr(self, '_arity', None)
101

    
102
    @arity.setter
103
    def arity(self, newarity):
104
        newarity = int(newarity)
105
        self._arity = newarity
106

    
107
    @property
108
    def default(self):
109
        """the value of this argument when not set"""
110
        if not hasattr(self, '_default'):
111
            self._default = False if self.arity == 0 else None
112
        return self._default
113

    
114
    @default.setter
115
    def default(self, newdefault):
116
        self._default = newdefault
117

    
118
    @property
119
    def value(self):
120
        """the value of the argument"""
121
        return getattr(self, '_value', self.default)
122

    
123
    @value.setter
124
    def value(self, newvalue):
125
        self._value = newvalue
126

    
127
    def update_parser(self, parser, name):
128
        """Update argument parser with self info"""
129
        action = 'append' if self.arity < 0\
130
            else 'store_true' if self.arity == 0\
131
            else 'store'
132
        parser.add_argument(*self.parsed_name, dest=name, action=action,
133
            default=self.default, help=self.help)
134

    
135
    def main(self):
136
        """Overide this method to give functionality to your args"""
137
        raise NotImplementedError
138

    
139

    
140
class ConfigArgument(Argument):
141
    """Manage a kamaki configuration (file)"""
142

    
143
    _config_file = None
144

    
145
    @property
146
    def value(self):
147
        """A Config object"""
148
        super(self.__class__, self).value
149
        return super(self.__class__, self).value
150

    
151
    @value.setter
152
    def value(self, config_file):
153
        if config_file:
154
            self._value = Config(config_file)
155
            self._config_file = config_file
156
        elif self._config_file:
157
            self._value = Config(self._config_file)
158
        else:
159
            self._value = Config()
160

    
161
    def get(self, group, term):
162
        """Get a configuration setting from the Config object"""
163
        return self.value.get(group, term)
164

    
165
    def get_groups(self):
166
        return self.value.apis()
167

    
168
_config_arg = ConfigArgument(1, 'Path to configuration file', '--config')
169

    
170

    
171
class CmdLineConfigArgument(Argument):
172
    """Set a run-time setting option (not persistent)"""
173

    
174
    def __init__(self, config_arg, help='', parsed_name=None, default=None):
175
        super(self.__class__, self).__init__(1, help, parsed_name, default)
176
        self._config_arg = config_arg
177

    
178
    @property
179
    def value(self):
180
        """A key=val option"""
181
        return super(self.__class__, self).value
182

    
183
    @value.setter
184
    def value(self, options):
185
        if options == self.default:
186
            return
187
        if not isinstance(options, list):
188
            options = [unicode(options)]
189
        for option in options:
190
            keypath, sep, val = option.partition('=')
191
            if not sep:
192
                raiseCLIError(CLISyntaxError('Argument Syntax Error '),
193
                    details=['%s is missing a "="',
194
                    ' (usage: -o section.key=val)' % option]
195
                )
196
            section, sep, key = keypath.partition('.')
197
        if not sep:
198
            key = section
199
            section = 'global'
200
        self._config_arg.value.override(
201
            section.strip(),
202
            key.strip(),
203
            val.strip())
204

    
205

    
206
class FlagArgument(Argument):
207
    """
208
    :value: true if set, false otherwise
209
    """
210

    
211
    def __init__(self, help='', parsed_name=None, default=False):
212
        super(FlagArgument, self).__init__(0, help, parsed_name, default)
213

    
214

    
215
class ValueArgument(Argument):
216
    """
217
    :value type: string
218
    :value returns: given value or default
219
    """
220

    
221
    def __init__(self, help='', parsed_name=None, default=None):
222
        super(ValueArgument, self).__init__(1, help, parsed_name, default)
223

    
224

    
225
class IntArgument(ValueArgument):
226

    
227
    @property
228
    def value(self):
229
        """integer (type checking)"""
230
        return getattr(self, '_value', self.default)
231

    
232
    @value.setter
233
    def value(self, newvalue):
234
        if newvalue == self.default:
235
            self._value = self.default
236
            return
237
        try:
238
            self._value = int(newvalue)
239
        except ValueError:
240
            raiseCLIError(CLISyntaxError('IntArgument Error',
241
                details=['Value %s not an int' % newvalue]))
242

    
243

    
244
class DateArgument(ValueArgument):
245
    """
246
    :value type: a string formated in an acceptable date format
247

248
    :value returns: same date in first of DATE_FORMATS
249
    """
250

    
251
    DATE_FORMATS = ["%a %b %d %H:%M:%S %Y",
252
        "%A, %d-%b-%y %H:%M:%S GMT",
253
        "%a, %d %b %Y %H:%M:%S GMT"]
254

    
255
    INPUT_FORMATS = DATE_FORMATS + ["%d-%m-%Y", "%H:%M:%S %d-%m-%Y"]
256

    
257
    @property
258
    def value(self):
259
        return getattr(self, '_value', self.default)
260

    
261
    @value.setter
262
    def value(self, newvalue):
263
        if newvalue is None:
264
            return
265
        self._value = self.format_date(newvalue)
266

    
267
    def format_date(self, datestr):
268
        for format in self.INPUT_FORMATS:
269
            try:
270
                t = dtm.strptime(datestr, format)
271
            except ValueError:
272
                continue
273
            self._value = t.strftime(self.DATE_FORMATS[0])
274
            return
275
        raiseCLIError(None,
276
            'Date Argument Error',
277
            details='%s not a valid date. correct formats:\n\t%s'\
278
            % (datestr, self.INPUT_FORMATS))
279

    
280

    
281
class VersionArgument(FlagArgument):
282
    """A flag argument with that prints current version"""
283

    
284
    @property
285
    def value(self):
286
        """bool"""
287
        return super(self.__class__, self).value
288

    
289
    @value.setter
290
    def value(self, newvalue):
291
        self._value = newvalue
292
        self.main()
293

    
294
    def main(self):
295
        """Print current version"""
296
        if self.value:
297
            import kamaki
298
            print('kamaki %s' % kamaki.__version__)
299

    
300

    
301
class KeyValueArgument(Argument):
302
    """A Value Argument that can be repeated
303

304
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
305
    """
306

    
307
    def __init__(self, help='', parsed_name=None, default=[]):
308
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
309

    
310
    @property
311
    def value(self):
312
        """
313
        :input: key=value
314
        :output: {'key1':'value1', 'key2':'value2', ...}
315
        """
316
        return super(KeyValueArgument, self).value
317

    
318
    @value.setter
319
    def value(self, keyvalue_pairs):
320
        self._value = {}
321
        for pair in keyvalue_pairs:
322
            key, sep, val = pair.partition('=')
323
            if not sep:
324
                raiseCLIError(CLISyntaxError('Argument syntax error '),
325
                    details='%s is missing a "=" (usage: key1=val1 )\n' % pair)
326
            self._value[key.strip()] = val.strip()
327

    
328

    
329
class ProgressBarArgument(FlagArgument):
330
    """Manage a progress bar"""
331

    
332
    def __init__(self, help='', parsed_name='', default=True):
333
        self.suffix = '%(percent)d%%'
334
        super(ProgressBarArgument, self).__init__(help, parsed_name, default)
335
        try:
336
            KamakiProgressBar
337
        except NameError:
338
            kloger.warning('no progress bar functionality')
339

    
340
    def clone(self):
341
        """Get a modifiable copy of this bar"""
342
        newarg = ProgressBarArgument(
343
            self.help,
344
            self.parsed_name,
345
            self.default)
346
        newarg._value = self._value
347
        return newarg
348

    
349
    def get_generator(self, message, message_len=25):
350
        """Get a generator to handle progress of the bar (gen.next())"""
351
        if self.value:
352
            return None
353
        try:
354
            self.bar = KamakiProgressBar()
355
        except NameError:
356
            self.value = None
357
            return self.value
358
        self.bar.message = message.ljust(message_len)
359
        self.bar.suffix = '%(percent)d%% - %(eta)ds'
360
        self.bar.start()
361

    
362
        def progress_gen(n):
363
            for i in self.bar.iter(range(int(n))):
364
                yield
365
            yield
366
        return progress_gen
367

    
368
    def finish(self):
369
        """Stop progress bar, return terminal cursor to user"""
370
        if self.value:
371
            return
372
        mybar = getattr(self, 'bar', None)
373
        if mybar:
374
            mybar.finish()
375

    
376

    
377
_arguments = dict(config=_config_arg,
378
    help=Argument(0, 'Show help message', ('-h', '--help')),
379
    debug=FlagArgument('Include debug output', ('-d', '--debug')),
380
    include=FlagArgument('Include raw connection data in the output',
381
        ('-i', '--include')),
382
    silent=FlagArgument('Do not output anything', ('-s', '--silent')),
383
    verbose=FlagArgument('More info at response', ('-v', '--verbose')),
384
    version=VersionArgument('Print current version', ('-V', '--version')),
385
    options=CmdLineConfigArgument(_config_arg,
386
        'Override a config value',
387
        ('-o', '--options'))
388
)
389
"""Initial command line interface arguments"""
390

    
391

    
392
"""
393
Mechanism:
394
    init_parser
395
    parse_known_args
396
    manage top-level user arguments input
397
    find user-requested command
398
    add command-specific arguments to dict
399
    update_arguments
400
"""
401

    
402

    
403
class ArgumentParseManager(object):
404
    """Manage (initialize and update) an ArgumentParser object"""
405

    
406
    parser = None
407
    _arguments = {}
408
    _parser_modified = False
409
    _parsed = None
410
    _unparsed = None
411

    
412
    def __init__(self, exe, arguments=None):
413
        """
414
        :param exe: (str) the basic command (e.g. 'kamaki')
415

416
        :param arguments: (dict) if given, overrides the global _argument as
417
            the parsers arguments specification
418
        """
419
        self.parser = ArgumentParser(add_help=False,
420
            formatter_class=RawDescriptionHelpFormatter)
421
        self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
422
        if arguments:
423
            self.arguments = arguments
424
        else:
425
            global _arguments
426
            self.arguments = _arguments
427
        self.parse()
428

    
429
    @property
430
    def syntax(self):
431
        """The command syntax (useful for help messages, descriptions, etc)"""
432
        return self.parser.prog
433

    
434
    @syntax.setter
435
    def syntax(self, new_syntax):
436
        self.parser.prog = new_syntax
437

    
438
    @property
439
    def arguments(self):
440
        """(dict) arguments the parser should be aware of"""
441
        return self._arguments
442

    
443
    @arguments.setter
444
    def arguments(self, new_arguments):
445
        if new_arguments:
446
            assert isinstance(new_arguments, dict)
447
        self._arguments = new_arguments
448
        self.update_parser()
449

    
450
    @property
451
    def parsed(self):
452
        """(Namespace) parser-matched terms"""
453
        if self._parser_modified:
454
            self.parse()
455
        return self._parsed
456

    
457
    @property
458
    def unparsed(self):
459
        """(list) parser-unmatched terms"""
460
        if self._parser_modified:
461
            self.parse()
462
        return self._unparsed
463

    
464
    def update_parser(self, arguments=None):
465
        """Load argument specifications to parser
466

467
        :param arguments: if not given, update self.arguments instead
468
        """
469
        if not arguments:
470
            arguments = self._arguments
471

    
472
        for name, arg in arguments.items():
473
            try:
474
                arg.update_parser(self.parser, name)
475
                self._parser_modified = True
476
            except ArgumentError:
477
                pass
478

    
479
    def update_arguments(self, new_arguments):
480
        """Add to / update existing arguments
481

482
        :param new_arguments: (dict)
483
        """
484
        if new_arguments:
485
            assert isinstance(new_arguments, dict)
486
            self._arguments.update(new_arguments)
487
            self.update_parser()
488

    
489
    def parse(self, new_args=None):
490
        """Do parse user input"""
491
        try:
492
            if new_args:
493
                self._parsed, unparsed = self.parser.parse_known_args(new_args)
494
            else:
495
                self._parsed, unparsed = self.parser.parse_known_args()
496
        except SystemExit:
497
            # deal with the fact that argparse error system is STUPID
498
            raiseCLIError(CLISyntaxError('Argument Syntax Error'))
499
        for name, arg in self.arguments.items():
500
            arg.value = getattr(self._parsed, name, arg.default)
501
        self._unparsed = []
502
        for term in unparsed:
503
            self._unparsed += split_input(' \'%s\' ' % term)
504
        self._parser_modified = False