Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument.py @ 0155548b

History | View | Annotate | Download (16.1 kB)

1
# Copyright 2012-2013 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

    
38
from datetime import datetime as dtm
39
from time import mktime
40

    
41
from logging import getLogger
42
from argparse import ArgumentParser, ArgumentError
43
from argparse import RawDescriptionHelpFormatter
44

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

    
55
log = getLogger(__name__)
56

    
57

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

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

    
67
        if help:
68
            self.help = help
69
        if parsed_name:
70
            self.parsed_name = parsed_name
71
        assert self.parsed_name, 'No parsed name for argument %s' % self
72
        self.default = default
73

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

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

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

    
94
    @help.setter
95
    def help(self, newhelp):
96
        self._help = '%s' % newhelp
97

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

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

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

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

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

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

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

    
140
    def main(self):
141
        """Overide this method to give functionality to your args"""
142
        raise NotImplementedError
143

    
144

    
145
class ConfigArgument(Argument):
146
    """Manage a kamaki configuration (file)"""
147

    
148
    _config_file = None
149

    
150
    @property
151
    def value(self):
152
        """A Config object"""
153
        super(self.__class__, self).value
154
        return super(self.__class__, self).value
155

    
156
    @value.setter
157
    def value(self, config_file):
158
        if config_file:
159
            self._value = Config(config_file)
160
            self._config_file = config_file
161
        elif self._config_file:
162
            self._value = Config(self._config_file)
163
        else:
164
            self._value = Config()
165

    
166
    def get(self, group, term):
167
        """Get a configuration setting from the Config object"""
168
        return self.value.get(group, term)
169

    
170
    def get_groups(self):
171
        suffix = '_cli'
172
        slen = len(suffix)
173
        return [term[:-slen] for term in self.value.keys('global') if (
174
            term.endswith(suffix))]
175

    
176
    def get_cli_specs(self):
177
        suffix = '_cli'
178
        slen = len(suffix)
179
        return [(k[:-slen], v) for k, v in self.value.items('global') if (
180
            k.endswith(suffix))]
181

    
182
    def get_global(self, option):
183
        return self.value.get_global(option)
184

    
185
    def get_cloud(self, cloud, option):
186
        return self.value.get_cloud(cloud, option)
187

    
188
_config_arg = ConfigArgument(
189
    1, 'Path to configuration file', ('-c', '--config'))
190

    
191

    
192
class CmdLineConfigArgument(Argument):
193
    """Set a run-time setting option (not persistent)"""
194

    
195
    def __init__(self, config_arg, help='', parsed_name=None, default=None):
196
        super(self.__class__, self).__init__(1, help, parsed_name, default)
197
        self._config_arg = config_arg
198

    
199
    @property
200
    def value(self):
201
        """A key=val option"""
202
        return super(self.__class__, self).value
203

    
204
    @value.setter
205
    def value(self, options):
206
        if options == self.default:
207
            return
208
        if not isinstance(options, list):
209
            options = ['%s' % options]
210
        for option in options:
211
            keypath, sep, val = option.partition('=')
212
            if not sep:
213
                raiseCLIError(
214
                    CLISyntaxError('Argument Syntax Error '),
215
                    details=[
216
                        '%s is missing a "="',
217
                        ' (usage: -o section.key=val)' % option])
218
            section, sep, key = keypath.partition('.')
219
        if not sep:
220
            key = section
221
            section = 'global'
222
        self._config_arg.value.override(
223
            section.strip(),
224
            key.strip(),
225
            val.strip())
226

    
227

    
228
class FlagArgument(Argument):
229
    """
230
    :value: true if set, false otherwise
231
    """
232

    
233
    def __init__(self, help='', parsed_name=None, default=False):
234
        super(FlagArgument, self).__init__(0, help, parsed_name, default)
235

    
236

    
237
class ValueArgument(Argument):
238
    """
239
    :value type: string
240
    :value returns: given value or default
241
    """
242

    
243
    def __init__(self, help='', parsed_name=None, default=None):
244
        super(ValueArgument, self).__init__(1, help, parsed_name, default)
245

    
246

    
247
class CommaSeparatedListArgument(ValueArgument):
248
    """
249
    :value type: string
250
    :value returns: list of the comma separated values
251
    """
252

    
253
    @property
254
    def value(self):
255
        return self._value or list()
256

    
257
    @value.setter
258
    def value(self, newvalue):
259
        self._value = newvalue.split(',') if newvalue else list()
260

    
261

    
262
class IntArgument(ValueArgument):
263

    
264
    @property
265
    def value(self):
266
        """integer (type checking)"""
267
        return getattr(self, '_value', self.default)
268

    
269
    @value.setter
270
    def value(self, newvalue):
271
        if newvalue == self.default:
272
            self._value = self.default
273
            return
274
        try:
275
            self._value = int(newvalue)
276
        except ValueError:
277
            raiseCLIError(CLISyntaxError(
278
                'IntArgument Error',
279
                details=['Value %s not an int' % newvalue]))
280

    
281

    
282
class DateArgument(ValueArgument):
283
    """
284
    :value type: a string formated in an acceptable date format
285

286
    :value returns: same date in first of DATE_FORMATS
287
    """
288

    
289
    DATE_FORMATS = [
290
        "%a %b %d %H:%M:%S %Y",
291
        "%A, %d-%b-%y %H:%M:%S GMT",
292
        "%a, %d %b %Y %H:%M:%S GMT"]
293

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

    
296
    @property
297
    def timestamp(self):
298
        v = getattr(self, '_value', self.default)
299
        return mktime(v.timetuple()) if v else None
300

    
301
    @property
302
    def formated(self):
303
        v = getattr(self, '_value', self.default)
304
        return v.strftime(self.DATE_FORMATS[0]) if v else None
305

    
306
    @property
307
    def value(self):
308
        return self.timestamp
309

    
310
    @value.setter
311
    def value(self, newvalue):
312
        if newvalue:
313
            self._value = self.format_date(newvalue)
314

    
315
    def format_date(self, datestr):
316
        for format in self.INPUT_FORMATS:
317
            try:
318
                t = dtm.strptime(datestr, format)
319
            except ValueError:
320
                continue
321
            return t  # .strftime(self.DATE_FORMATS[0])
322
        raiseCLIError(
323
            None,
324
            'Date Argument Error',
325
            details='%s not a valid date. correct formats:\n\t%s' % (
326
                datestr, self.INPUT_FORMATS))
327

    
328

    
329
class VersionArgument(FlagArgument):
330
    """A flag argument with that prints current version"""
331

    
332
    @property
333
    def value(self):
334
        """bool"""
335
        return super(self.__class__, self).value
336

    
337
    @value.setter
338
    def value(self, newvalue):
339
        self._value = newvalue
340
        self.main()
341

    
342
    def main(self):
343
        """Print current version"""
344
        if self.value:
345
            import kamaki
346
            print('kamaki %s' % kamaki.__version__)
347

    
348

    
349
class KeyValueArgument(Argument):
350
    """A Value Argument that can be repeated
351

352
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
353
    """
354

    
355
    def __init__(self, help='', parsed_name=None, default=[]):
356
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
357

    
358
    @property
359
    def value(self):
360
        """
361
        :input: key=value
362
        :output: {'key1':'value1', 'key2':'value2', ...}
363
        """
364
        return super(KeyValueArgument, self).value
365

    
366
    @value.setter
367
    def value(self, keyvalue_pairs):
368
        self._value = {}
369
        for pair in keyvalue_pairs:
370
            key, sep, val = pair.partition('=')
371
            if not sep:
372
                raiseCLIError(
373
                    CLISyntaxError('Argument syntax error '),
374
                    details='%s is missing a "=" (usage: key1=val1 )\n' % pair)
375
            self._value[key.strip()] = val.strip()
376

    
377

    
378
class ProgressBarArgument(FlagArgument):
379
    """Manage a progress bar"""
380

    
381
    def __init__(self, help='', parsed_name='', default=True):
382
        self.suffix = '%(percent)d%%'
383
        super(ProgressBarArgument, self).__init__(help, parsed_name, default)
384
        try:
385
            KamakiProgressBar
386
        except NameError:
387
            log.warning('WARNING: no progress bar functionality')
388

    
389
    def clone(self):
390
        """Get a modifiable copy of this bar"""
391
        newarg = ProgressBarArgument(
392
            self.help,
393
            self.parsed_name,
394
            self.default)
395
        newarg._value = self._value
396
        return newarg
397

    
398
    def get_generator(self, message, message_len=25):
399
        """Get a generator to handle progress of the bar (gen.next())"""
400
        if self.value:
401
            return None
402
        try:
403
            self.bar = KamakiProgressBar()
404
        except NameError:
405
            self.value = None
406
            return self.value
407
        self.bar.message = message.ljust(message_len)
408
        self.bar.suffix = '%(percent)d%% - %(eta)ds'
409
        self.bar.start()
410

    
411
        def progress_gen(n):
412
            for i in self.bar.iter(range(int(n))):
413
                yield
414
            yield
415
        return progress_gen
416

    
417
    def finish(self):
418
        """Stop progress bar, return terminal cursor to user"""
419
        if self.value:
420
            return
421
        mybar = getattr(self, 'bar', None)
422
        if mybar:
423
            mybar.finish()
424

    
425

    
426
_arguments = dict(
427
    config=_config_arg,
428
    cloud=ValueArgument('Chose a cloud to connect to', ('--cloud')),
429
    help=Argument(0, 'Show help message', ('-h', '--help')),
430
    debug=FlagArgument('Include debug output', ('-d', '--debug')),
431
    include=FlagArgument(
432
        'Include raw connection data in the output', ('-i', '--include')),
433
    silent=FlagArgument('Do not output anything', ('-s', '--silent')),
434
    verbose=FlagArgument('More info at response', ('-v', '--verbose')),
435
    version=VersionArgument('Print current version', ('-V', '--version')),
436
    options=CmdLineConfigArgument(
437
        _config_arg, 'Override a config value', ('-o', '--options'))
438
)
439

    
440

    
441
#  Initial command line interface arguments
442

    
443

    
444
class ArgumentParseManager(object):
445
    """Manage (initialize and update) an ArgumentParser object"""
446

    
447
    parser = None
448
    _arguments = {}
449
    _parser_modified = False
450
    _parsed = None
451
    _unparsed = None
452

    
453
    def __init__(self, exe, arguments=None):
454
        """
455
        :param exe: (str) the basic command (e.g. 'kamaki')
456

457
        :param arguments: (dict) if given, overrides the global _argument as
458
            the parsers arguments specification
459
        """
460
        self.parser = ArgumentParser(
461
            add_help=False, formatter_class=RawDescriptionHelpFormatter)
462
        self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
463
        if arguments:
464
            self.arguments = arguments
465
        else:
466
            global _arguments
467
            self.arguments = _arguments
468
        self.parse()
469

    
470
    @property
471
    def syntax(self):
472
        """The command syntax (useful for help messages, descriptions, etc)"""
473
        return self.parser.prog
474

    
475
    @syntax.setter
476
    def syntax(self, new_syntax):
477
        self.parser.prog = new_syntax
478

    
479
    @property
480
    def arguments(self):
481
        """(dict) arguments the parser should be aware of"""
482
        return self._arguments
483

    
484
    @arguments.setter
485
    def arguments(self, new_arguments):
486
        if new_arguments:
487
            assert isinstance(new_arguments, dict)
488
        self._arguments = new_arguments
489
        self.update_parser()
490

    
491
    @property
492
    def parsed(self):
493
        """(Namespace) parser-matched terms"""
494
        if self._parser_modified:
495
            self.parse()
496
        return self._parsed
497

    
498
    @property
499
    def unparsed(self):
500
        """(list) parser-unmatched terms"""
501
        if self._parser_modified:
502
            self.parse()
503
        return self._unparsed
504

    
505
    def update_parser(self, arguments=None):
506
        """Load argument specifications to parser
507

508
        :param arguments: if not given, update self.arguments instead
509
        """
510
        if not arguments:
511
            arguments = self._arguments
512

    
513
        for name, arg in arguments.items():
514
            try:
515
                arg.update_parser(self.parser, name)
516
                self._parser_modified = True
517
            except ArgumentError:
518
                pass
519

    
520
    def update_arguments(self, new_arguments):
521
        """Add to / update existing arguments
522

523
        :param new_arguments: (dict)
524
        """
525
        if new_arguments:
526
            assert isinstance(new_arguments, dict)
527
            self._arguments.update(new_arguments)
528
            self.update_parser()
529

    
530
    def parse(self, new_args=None):
531
        """Parse user input"""
532
        try:
533
            pkargs = (new_args,) if new_args else ()
534
            self._parsed, unparsed = self.parser.parse_known_args(*pkargs)
535
        except SystemExit:
536
            raiseCLIError(CLISyntaxError('Argument Syntax Error'))
537
        for name, arg in self.arguments.items():
538
            arg.value = getattr(self._parsed, name, arg.default)
539
        self._unparsed = []
540
        for term in unparsed:
541
            self._unparsed += split_input(' \'%s\' ' % term)
542
        self._parser_modified = False