Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument.py @ f724cd35

History | View | Annotate | Download (15.6 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
    help=Argument(0, 'Show help message', ('-h', '--help')),
402
    debug=FlagArgument('Include debug output', ('-d', '--debug')),
403
    include=FlagArgument(
404
        'Include raw connection data in the output',
405
        ('-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,
411
        'Override a config value',
412
        ('-o', '--options'))
413
)
414
"""Initial command line interface arguments"""
415

    
416

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

    
427

    
428
class ArgumentParseManager(object):
429
    """Manage (initialize and update) an ArgumentParser object"""
430

    
431
    parser = None
432
    _arguments = {}
433
    _parser_modified = False
434
    _parsed = None
435
    _unparsed = None
436

    
437
    def __init__(self, exe, arguments=None):
438
        """
439
        :param exe: (str) the basic command (e.g. 'kamaki')
440

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

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

    
460
    @syntax.setter
461
    def syntax(self, new_syntax):
462
        self.parser.prog = new_syntax
463

    
464
    @property
465
    def arguments(self):
466
        """(dict) arguments the parser should be aware of"""
467
        return self._arguments
468

    
469
    @arguments.setter
470
    def arguments(self, new_arguments):
471
        if new_arguments:
472
            assert isinstance(new_arguments, dict)
473
        self._arguments = new_arguments
474
        self.update_parser()
475

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

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

    
490
    def update_parser(self, arguments=None):
491
        """Load argument specifications to parser
492

493
        :param arguments: if not given, update self.arguments instead
494
        """
495
        if not arguments:
496
            arguments = self._arguments
497

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

    
505
    def update_arguments(self, new_arguments):
506
        """Add to / update existing arguments
507

508
        :param new_arguments: (dict)
509
        """
510
        if new_arguments:
511
            assert isinstance(new_arguments, dict)
512
            self._arguments.update(new_arguments)
513
            self.update_parser()
514

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