Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument.py @ 8c54338a

History | View | Annotate | Download (15.5 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

    
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
        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('%s' % 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 = '%s' % 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(
133
            *self.parsed_name,
134
            dest=name,
135
            action=action,
136
            default=self.default,
137
            help=self.help)
138

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

    
143

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

    
147
    _config_file = None
148

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

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

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

    
169
    def get_groups(self):
170
        return self.value.keys('cli')
171

    
172
    def get_cli_specs(self):
173
        return self.value.items('cli')
174

    
175
_config_arg = ConfigArgument(
176
    1, 'Path to configuration file',
177
    ('-c', '--config'))
178

    
179

    
180
class CmdLineConfigArgument(Argument):
181
    """Set a run-time setting option (not persistent)"""
182

    
183
    def __init__(self, config_arg, help='', parsed_name=None, default=None):
184
        super(self.__class__, self).__init__(1, help, parsed_name, default)
185
        self._config_arg = config_arg
186

    
187
    @property
188
    def value(self):
189
        """A key=val option"""
190
        return super(self.__class__, self).value
191

    
192
    @value.setter
193
    def value(self, options):
194
        if options == self.default:
195
            return
196
        if not isinstance(options, list):
197
            options = ['%s' % options]
198
        for option in options:
199
            keypath, sep, val = option.partition('=')
200
            if not sep:
201
                raiseCLIError(
202
                    CLISyntaxError('Argument Syntax Error '),
203
                    details=[
204
                        '%s is missing a "="',
205
                        ' (usage: -o section.key=val)' % option])
206
            section, sep, key = keypath.partition('.')
207
        if not sep:
208
            key = section
209
            section = 'global'
210
        self._config_arg.value.override(
211
            section.strip(),
212
            key.strip(),
213
            val.strip())
214

    
215

    
216
class FlagArgument(Argument):
217
    """
218
    :value: true if set, false otherwise
219
    """
220

    
221
    def __init__(self, help='', parsed_name=None, default=False):
222
        super(FlagArgument, self).__init__(0, help, parsed_name, default)
223

    
224

    
225
class ValueArgument(Argument):
226
    """
227
    :value type: string
228
    :value returns: given value or default
229
    """
230

    
231
    def __init__(self, help='', parsed_name=None, default=None):
232
        super(ValueArgument, self).__init__(1, help, parsed_name, default)
233

    
234

    
235
class IntArgument(ValueArgument):
236

    
237
    @property
238
    def value(self):
239
        """integer (type checking)"""
240
        return getattr(self, '_value', self.default)
241

    
242
    @value.setter
243
    def value(self, newvalue):
244
        if newvalue == self.default:
245
            self._value = self.default
246
            return
247
        try:
248
            self._value = int(newvalue)
249
        except ValueError:
250
            raiseCLIError(CLISyntaxError(
251
                'IntArgument Error',
252
                details=['Value %s not an int' % newvalue]))
253

    
254

    
255
class DateArgument(ValueArgument):
256
    """
257
    :value type: a string formated in an acceptable date format
258

259
    :value returns: same date in first of DATE_FORMATS
260
    """
261

    
262
    DATE_FORMATS = [
263
        "%a %b %d %H:%M:%S %Y",
264
        "%A, %d-%b-%y %H:%M:%S GMT",
265
        "%a, %d %b %Y %H:%M:%S GMT"]
266

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

    
269
    @property
270
    def timestamp(self):
271
        v = getattr(self, '_value', self.default)
272
        return mktime(v.timetuple()) if v else None
273

    
274
    @property
275
    def formated(self):
276
        v = getattr(self, '_value', self.default)
277
        return v.strftime(self.DATE_FORMATS[0]) if v else None
278

    
279
    @property
280
    def value(self):
281
        return self.timestamp
282

    
283
    @value.setter
284
    def value(self, newvalue):
285
        if newvalue:
286
            self._value = self.format_date(newvalue)
287

    
288
    def format_date(self, datestr):
289
        for format in self.INPUT_FORMATS:
290
            try:
291
                t = dtm.strptime(datestr, format)
292
            except ValueError:
293
                continue
294
            return t  # .strftime(self.DATE_FORMATS[0])
295
        raiseCLIError(
296
            None,
297
            'Date Argument Error',
298
            details='%s not a valid date. correct formats:\n\t%s' % (
299
                datestr, self.INPUT_FORMATS))
300

    
301

    
302
class VersionArgument(FlagArgument):
303
    """A flag argument with that prints current version"""
304

    
305
    @property
306
    def value(self):
307
        """bool"""
308
        return super(self.__class__, self).value
309

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

    
315
    def main(self):
316
        """Print current version"""
317
        if self.value:
318
            import kamaki
319
            print('kamaki %s' % kamaki.__version__)
320

    
321

    
322
class KeyValueArgument(Argument):
323
    """A Value Argument that can be repeated
324

325
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
326
    """
327

    
328
    def __init__(self, help='', parsed_name=None, default=[]):
329
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
330

    
331
    @property
332
    def value(self):
333
        """
334
        :input: key=value
335
        :output: {'key1':'value1', 'key2':'value2', ...}
336
        """
337
        return super(KeyValueArgument, self).value
338

    
339
    @value.setter
340
    def value(self, keyvalue_pairs):
341
        self._value = {}
342
        for pair in keyvalue_pairs:
343
            key, sep, val = pair.partition('=')
344
            if not sep:
345
                raiseCLIError(
346
                    CLISyntaxError('Argument syntax error '),
347
                    details='%s is missing a "=" (usage: key1=val1 )\n' % pair)
348
            self._value[key.strip()] = val.strip()
349

    
350

    
351
class ProgressBarArgument(FlagArgument):
352
    """Manage a progress bar"""
353

    
354
    def __init__(self, help='', parsed_name='', default=True):
355
        self.suffix = '%(percent)d%%'
356
        super(ProgressBarArgument, self).__init__(help, parsed_name, default)
357
        try:
358
            KamakiProgressBar
359
        except NameError:
360
            log.warning('WARNING: no progress bar functionality')
361

    
362
    def clone(self):
363
        """Get a modifiable copy of this bar"""
364
        newarg = ProgressBarArgument(
365
            self.help,
366
            self.parsed_name,
367
            self.default)
368
        newarg._value = self._value
369
        return newarg
370

    
371
    def get_generator(self, message, message_len=25):
372
        """Get a generator to handle progress of the bar (gen.next())"""
373
        if self.value:
374
            return None
375
        try:
376
            self.bar = KamakiProgressBar()
377
        except NameError:
378
            self.value = None
379
            return self.value
380
        self.bar.message = message.ljust(message_len)
381
        self.bar.suffix = '%(percent)d%% - %(eta)ds'
382
        self.bar.start()
383

    
384
        def progress_gen(n):
385
            for i in self.bar.iter(range(int(n))):
386
                yield
387
            yield
388
        return progress_gen
389

    
390
    def finish(self):
391
        """Stop progress bar, return terminal cursor to user"""
392
        if self.value:
393
            return
394
        mybar = getattr(self, 'bar', None)
395
        if mybar:
396
            mybar.finish()
397

    
398

    
399
_arguments = dict(
400
    config=_config_arg,
401
    cloud=ValueArgument('Chose a remote cloud to connect to', ('--cloud')),
402
    help=Argument(0, 'Show help message', ('-h', '--help')),
403
    debug=FlagArgument('Include debug output', ('-d', '--debug')),
404
    include=FlagArgument(
405
        'Include raw connection data in the output', ('-i', '--include')),
406
    silent=FlagArgument('Do not output anything', ('-s', '--silent')),
407
    verbose=FlagArgument('More info at response', ('-v', '--verbose')),
408
    version=VersionArgument('Print current version', ('-V', '--version')),
409
    options=CmdLineConfigArgument(
410
        _config_arg, 'Override a config value', ('-o', '--options'))
411
)
412

    
413

    
414
#  Initial command line interface arguments
415

    
416

    
417
class ArgumentParseManager(object):
418
    """Manage (initialize and update) an ArgumentParser object"""
419

    
420
    parser = None
421
    _arguments = {}
422
    _parser_modified = False
423
    _parsed = None
424
    _unparsed = None
425

    
426
    def __init__(self, exe, arguments=None):
427
        """
428
        :param exe: (str) the basic command (e.g. 'kamaki')
429

430
        :param arguments: (dict) if given, overrides the global _argument as
431
            the parsers arguments specification
432
        """
433
        self.parser = ArgumentParser(
434
            add_help=False,
435
            formatter_class=RawDescriptionHelpFormatter)
436
        self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
437
        if arguments:
438
            self.arguments = arguments
439
        else:
440
            global _arguments
441
            self.arguments = _arguments
442
        self.parse()
443

    
444
    @property
445
    def syntax(self):
446
        """The command syntax (useful for help messages, descriptions, etc)"""
447
        return self.parser.prog
448

    
449
    @syntax.setter
450
    def syntax(self, new_syntax):
451
        self.parser.prog = new_syntax
452

    
453
    @property
454
    def arguments(self):
455
        """(dict) arguments the parser should be aware of"""
456
        return self._arguments
457

    
458
    @arguments.setter
459
    def arguments(self, new_arguments):
460
        if new_arguments:
461
            assert isinstance(new_arguments, dict)
462
        self._arguments = new_arguments
463
        self.update_parser()
464

    
465
    @property
466
    def parsed(self):
467
        """(Namespace) parser-matched terms"""
468
        if self._parser_modified:
469
            self.parse()
470
        return self._parsed
471

    
472
    @property
473
    def unparsed(self):
474
        """(list) parser-unmatched terms"""
475
        if self._parser_modified:
476
            self.parse()
477
        return self._unparsed
478

    
479
    def update_parser(self, arguments=None):
480
        """Load argument specifications to parser
481

482
        :param arguments: if not given, update self.arguments instead
483
        """
484
        if not arguments:
485
            arguments = self._arguments
486

    
487
        for name, arg in arguments.items():
488
            try:
489
                arg.update_parser(self.parser, name)
490
                self._parser_modified = True
491
            except ArgumentError:
492
                pass
493

    
494
    def update_arguments(self, new_arguments):
495
        """Add to / update existing arguments
496

497
        :param new_arguments: (dict)
498
        """
499
        if new_arguments:
500
            assert isinstance(new_arguments, dict)
501
            self._arguments.update(new_arguments)
502
            self.update_parser()
503

    
504
    def parse(self, new_args=None):
505
        """Do parse user input"""
506
        try:
507
            if new_args:
508
                self._parsed, unparsed = self.parser.parse_known_args(new_args)
509
            else:
510
                self._parsed, unparsed = self.parser.parse_known_args()
511
        except SystemExit:
512
            # deal with the fact that argparse error system is STUPID
513
            raiseCLIError(CLISyntaxError('Argument Syntax Error'))
514
        for name, arg in self.arguments.items():
515
            arg.value = getattr(self._parsed, name, arg.default)
516
        self._unparsed = []
517
        for term in unparsed:
518
            self._unparsed += split_input(' \'%s\' ' % term)
519
        self._parser_modified = False