Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument.py @ 9986e569

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
from kamaki.logger import get_logger
38
from datetime import datetime as dtm
39
from time import mktime
40

    
41

    
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 = get_logger('kamaki.cli')
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.apis()
171

    
172
_config_arg = ConfigArgument(
173
    1, 'Path to configuration file',
174
    ('-c', '--config'))
175

    
176

    
177
class CmdLineConfigArgument(Argument):
178
    """Set a run-time setting option (not persistent)"""
179

    
180
    def __init__(self, config_arg, help='', parsed_name=None, default=None):
181
        super(self.__class__, self).__init__(1, help, parsed_name, default)
182
        self._config_arg = config_arg
183

    
184
    @property
185
    def value(self):
186
        """A key=val option"""
187
        return super(self.__class__, self).value
188

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

    
212

    
213
class FlagArgument(Argument):
214
    """
215
    :value: true if set, false otherwise
216
    """
217

    
218
    def __init__(self, help='', parsed_name=None, default=False):
219
        super(FlagArgument, self).__init__(0, help, parsed_name, default)
220

    
221

    
222
class ValueArgument(Argument):
223
    """
224
    :value type: string
225
    :value returns: given value or default
226
    """
227

    
228
    def __init__(self, help='', parsed_name=None, default=None):
229
        super(ValueArgument, self).__init__(1, help, parsed_name, default)
230

    
231

    
232
class IntArgument(ValueArgument):
233

    
234
    @property
235
    def value(self):
236
        """integer (type checking)"""
237
        return getattr(self, '_value', self.default)
238

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

    
251

    
252
class DateArgument(ValueArgument):
253
    """
254
    :value type: a string formated in an acceptable date format
255

256
    :value returns: same date in first of DATE_FORMATS
257
    """
258

    
259
    DATE_FORMATS = [
260
        "%a %b %d %H:%M:%S %Y",
261
        "%A, %d-%b-%y %H:%M:%S GMT",
262
        "%a, %d %b %Y %H:%M:%S GMT"]
263

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

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

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

    
276
    @property
277
    def value(self):
278
        return self.timestamp
279

    
280
    @value.setter
281
    def value(self, newvalue):
282
        if newvalue:
283
            self._value = self.format_date(newvalue)
284

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

    
298

    
299
class VersionArgument(FlagArgument):
300
    """A flag argument with that prints current version"""
301

    
302
    @property
303
    def value(self):
304
        """bool"""
305
        return super(self.__class__, self).value
306

    
307
    @value.setter
308
    def value(self, newvalue):
309
        self._value = newvalue
310
        self.main()
311

    
312
    def main(self):
313
        """Print current version"""
314
        if self.value:
315
            import kamaki
316
            print('kamaki %s' % kamaki.__version__)
317

    
318

    
319
class KeyValueArgument(Argument):
320
    """A Value Argument that can be repeated
321

322
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
323
    """
324

    
325
    def __init__(self, help='', parsed_name=None, default=[]):
326
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
327

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

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

    
347

    
348
class ProgressBarArgument(FlagArgument):
349
    """Manage a progress bar"""
350

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

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

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

    
381
        def progress_gen(n):
382
            for i in self.bar.iter(range(int(n))):
383
                yield
384
            yield
385
        return progress_gen
386

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

    
395

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

    
413

    
414
"""
415
Mechanism:
416
    init_parser
417
    parse_known_args
418
    manage top-level user arguments input
419
    find user-requested command
420
    add command-specific arguments to dict
421
    update_arguments
422
"""
423

    
424

    
425
class ArgumentParseManager(object):
426
    """Manage (initialize and update) an ArgumentParser object"""
427

    
428
    parser = None
429
    _arguments = {}
430
    _parser_modified = False
431
    _parsed = None
432
    _unparsed = None
433

    
434
    def __init__(self, exe, arguments=None):
435
        """
436
        :param exe: (str) the basic command (e.g. 'kamaki')
437

438
        :param arguments: (dict) if given, overrides the global _argument as
439
            the parsers arguments specification
440
        """
441
        self.parser = ArgumentParser(
442
            add_help=False,
443
            formatter_class=RawDescriptionHelpFormatter)
444
        self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
445
        if arguments:
446
            self.arguments = arguments
447
        else:
448
            global _arguments
449
            self.arguments = _arguments
450
        self.parse()
451

    
452
    @property
453
    def syntax(self):
454
        """The command syntax (useful for help messages, descriptions, etc)"""
455
        return self.parser.prog
456

    
457
    @syntax.setter
458
    def syntax(self, new_syntax):
459
        self.parser.prog = new_syntax
460

    
461
    @property
462
    def arguments(self):
463
        """(dict) arguments the parser should be aware of"""
464
        return self._arguments
465

    
466
    @arguments.setter
467
    def arguments(self, new_arguments):
468
        if new_arguments:
469
            assert isinstance(new_arguments, dict)
470
        self._arguments = new_arguments
471
        self.update_parser()
472

    
473
    @property
474
    def parsed(self):
475
        """(Namespace) parser-matched terms"""
476
        if self._parser_modified:
477
            self.parse()
478
        return self._parsed
479

    
480
    @property
481
    def unparsed(self):
482
        """(list) parser-unmatched terms"""
483
        if self._parser_modified:
484
            self.parse()
485
        return self._unparsed
486

    
487
    def update_parser(self, arguments=None):
488
        """Load argument specifications to parser
489

490
        :param arguments: if not given, update self.arguments instead
491
        """
492
        if not arguments:
493
            arguments = self._arguments
494

    
495
        for name, arg in arguments.items():
496
            try:
497
                arg.update_parser(self.parser, name)
498
                self._parser_modified = True
499
            except ArgumentError:
500
                pass
501

    
502
    def update_arguments(self, new_arguments):
503
        """Add to / update existing arguments
504

505
        :param new_arguments: (dict)
506
        """
507
        if new_arguments:
508
            assert isinstance(new_arguments, dict)
509
            self._arguments.update(new_arguments)
510
            self.update_parser()
511

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