Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument.py @ a517ff50

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 logging import getLogger
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
kloger = getLogger('kamaki')
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(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 = ['%s' % 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 timestamp(self):
266
        v = getattr(self, '_value', self.default)
267
        return mktime(v.timetuple()) if v else None
268

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

    
274
    @property
275
    def value(self):
276
        return self.timestamp
277

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

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

    
296

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

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

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

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

    
316

    
317
class KeyValueArgument(Argument):
318
    """A Value Argument that can be repeated
319

320
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
321
    """
322

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

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

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

    
345

    
346
class ProgressBarArgument(FlagArgument):
347
    """Manage a progress bar"""
348

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

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

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

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

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

    
393

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

    
411

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

    
422

    
423
class ArgumentParseManager(object):
424
    """Manage (initialize and update) an ArgumentParser object"""
425

    
426
    parser = None
427
    _arguments = {}
428
    _parser_modified = False
429
    _parsed = None
430
    _unparsed = None
431

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

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

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

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

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

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

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

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

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

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

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

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

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

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