Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument.py @ 24ff0a35

History | View | Annotate | Download (15.3 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(
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(1, 'Path to configuration file', '--config')
173

    
174

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

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

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

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

    
210

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

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

    
219

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

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

    
229

    
230
class IntArgument(ValueArgument):
231

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

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

    
249

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

254
    :value returns: same date in first of DATE_FORMATS
255
    """
256

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

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

    
264
    @property
265
    def value(self):
266
        return getattr(self, '_value', self.default)
267

    
268
    @value.setter
269
    def value(self, newvalue):
270
        if newvalue is None:
271
            return
272
        self._value = self.format_date(newvalue)
273

    
274
    def format_date(self, datestr):
275
        for format in self.INPUT_FORMATS:
276
            try:
277
                t = dtm.strptime(datestr, format)
278
            except ValueError:
279
                continue
280
            self._value = t.strftime(self.DATE_FORMATS[0])
281
            return
282
        raiseCLIError(
283
            None,
284
            'Date Argument Error',
285
            details='%s not a valid date. correct formats:\n\t%s' % (
286
                datestr, self.INPUT_FORMATS))
287

    
288

    
289
class VersionArgument(FlagArgument):
290
    """A flag argument with that prints current version"""
291

    
292
    @property
293
    def value(self):
294
        """bool"""
295
        return super(self.__class__, self).value
296

    
297
    @value.setter
298
    def value(self, newvalue):
299
        self._value = newvalue
300
        self.main()
301

    
302
    def main(self):
303
        """Print current version"""
304
        if self.value:
305
            import kamaki
306
            print('kamaki %s' % kamaki.__version__)
307

    
308

    
309
class KeyValueArgument(Argument):
310
    """A Value Argument that can be repeated
311

312
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
313
    """
314

    
315
    def __init__(self, help='', parsed_name=None, default=[]):
316
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
317

    
318
    @property
319
    def value(self):
320
        """
321
        :input: key=value
322
        :output: {'key1':'value1', 'key2':'value2', ...}
323
        """
324
        return super(KeyValueArgument, self).value
325

    
326
    @value.setter
327
    def value(self, keyvalue_pairs):
328
        self._value = {}
329
        for pair in keyvalue_pairs:
330
            key, sep, val = pair.partition('=')
331
            if not sep:
332
                raiseCLIError(
333
                    CLISyntaxError('Argument syntax error '),
334
                    details='%s is missing a "=" (usage: key1=val1 )\n' % pair)
335
            self._value[key.strip()] = val.strip()
336

    
337

    
338
class ProgressBarArgument(FlagArgument):
339
    """Manage a progress bar"""
340

    
341
    def __init__(self, help='', parsed_name='', default=True):
342
        self.suffix = '%(percent)d%%'
343
        super(ProgressBarArgument, self).__init__(help, parsed_name, default)
344
        try:
345
            KamakiProgressBar
346
        except NameError:
347
            kloger.warning('no progress bar functionality')
348

    
349
    def clone(self):
350
        """Get a modifiable copy of this bar"""
351
        newarg = ProgressBarArgument(
352
            self.help,
353
            self.parsed_name,
354
            self.default)
355
        newarg._value = self._value
356
        return newarg
357

    
358
    def get_generator(self, message, message_len=25):
359
        """Get a generator to handle progress of the bar (gen.next())"""
360
        if self.value:
361
            return None
362
        try:
363
            self.bar = KamakiProgressBar()
364
        except NameError:
365
            self.value = None
366
            return self.value
367
        self.bar.message = message.ljust(message_len)
368
        self.bar.suffix = '%(percent)d%% - %(eta)ds'
369
        self.bar.start()
370

    
371
        def progress_gen(n):
372
            for i in self.bar.iter(range(int(n))):
373
                yield
374
            yield
375
        return progress_gen
376

    
377
    def finish(self):
378
        """Stop progress bar, return terminal cursor to user"""
379
        if self.value:
380
            return
381
        mybar = getattr(self, 'bar', None)
382
        if mybar:
383
            mybar.finish()
384

    
385

    
386
_arguments = dict(
387
    config=_config_arg,
388
    help=Argument(0, 'Show help message', ('-h', '--help')),
389
    debug=FlagArgument('Include debug output', ('-d', '--debug')),
390
    include=FlagArgument(
391
        'Include raw connection data in the output',
392
        ('-i', '--include')),
393
    silent=FlagArgument('Do not output anything', ('-s', '--silent')),
394
    verbose=FlagArgument('More info at response', ('-v', '--verbose')),
395
    version=VersionArgument('Print current version', ('-V', '--version')),
396
    options=CmdLineConfigArgument(
397
        _config_arg,
398
        'Override a config value',
399
        ('-o', '--options'))
400
)
401
"""Initial command line interface arguments"""
402

    
403

    
404
"""
405
Mechanism:
406
    init_parser
407
    parse_known_args
408
    manage top-level user arguments input
409
    find user-requested command
410
    add command-specific arguments to dict
411
    update_arguments
412
"""
413

    
414

    
415
class ArgumentParseManager(object):
416
    """Manage (initialize and update) an ArgumentParser object"""
417

    
418
    parser = None
419
    _arguments = {}
420
    _parser_modified = False
421
    _parsed = None
422
    _unparsed = None
423

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

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

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

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

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

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

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

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

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

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

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

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

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

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