Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument / __init__.py @ 5286e2c3

History | View | Annotate | Download (15.4 kB)

1
# Copyright 2012-2013 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 top-level 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
        self.help = '%s' % help or ''
67
        self.parsed_name = parsed_name
68
        self.default = default or (None if self.arity else False)
69

    
70
    @property
71
    def parsed_name(self):
72
        """
73
        :returns: (str) of the form --smth or -s is recognised as a call to an
74
            argument instance
75
        """
76
        return getattr(self, '_parsed_name', [])
77

    
78
    @parsed_name.setter
79
    def parsed_name(self, newname):
80
        assert newname, 'No parsed name for argument %s' % self
81
        self._parsed_name = getattr(self, '_parsed_name', [])
82
        if isinstance(newname, list) or isinstance(newname, tuple):
83
            self._parsed_name += list(newname)
84
        else:
85
            self._parsed_name.append('%s' % newname)
86

    
87
    @property
88
    def value(self):
89
        """the value of the argument"""
90
        return getattr(self, '_value', self.default)
91

    
92
    @value.setter
93
    def value(self, newvalue):
94
        self._value = newvalue
95

    
96
    def update_parser(self, parser, name):
97
        """Update argument parser with self info"""
98
        action = 'append' if self.arity < 0\
99
            else 'store_true' if self.arity == 0\
100
            else 'store'
101
        parser.add_argument(
102
            *self.parsed_name,
103
            dest=name,
104
            action=action,
105
            default=self.default,
106
            help=self.help)
107

    
108
    def main(self):
109
        """Overide this method to give functionality to your args"""
110
        raise NotImplementedError
111

    
112

    
113
class ConfigArgument(Argument):
114
    """Manage a kamaki configuration (file)"""
115

    
116
    _config_file = None
117

    
118
    @property
119
    def value(self):
120
        """A Config object"""
121
        super(self.__class__, self).value
122
        return super(self.__class__, self).value
123

    
124
    @value.setter
125
    def value(self, config_file):
126
        if config_file:
127
            self._value = Config(config_file)
128
            self._config_file = config_file
129
        elif self._config_file:
130
            self._value = Config(self._config_file)
131
        else:
132
            self._value = Config()
133

    
134
    def get(self, group, term):
135
        """Get a configuration setting from the Config object"""
136
        return self.value.get(group, term)
137

    
138
    def get_groups(self):
139
        suffix = '_cli'
140
        slen = len(suffix)
141
        return [term[:-slen] for term in self.value.keys('global') if (
142
            term.endswith(suffix))]
143

    
144
    def get_cli_specs(self):
145
        suffix = '_cli'
146
        slen = len(suffix)
147
        return [(k[:-slen], v) for k, v in self.value.items('global') if (
148
            k.endswith(suffix))]
149

    
150
    def get_global(self, option):
151
        return self.value.get_global(option)
152

    
153
    def get_cloud(self, cloud, option):
154
        return self.value.get_cloud(cloud, option)
155

    
156
_config_arg = ConfigArgument(
157
    1, 'Path to configuration file', ('-c', '--config'))
158

    
159

    
160
class CmdLineConfigArgument(Argument):
161
    """Set a run-time setting option (not persistent)"""
162

    
163
    def __init__(self, config_arg, help='', parsed_name=None, default=None):
164
        super(self.__class__, self).__init__(1, help, parsed_name, default)
165
        self._config_arg = config_arg
166

    
167
    @property
168
    def value(self):
169
        """A key=val option"""
170
        return super(self.__class__, self).value
171

    
172
    @value.setter
173
    def value(self, options):
174
        if options == self.default:
175
            return
176
        if not isinstance(options, list):
177
            options = ['%s' % options]
178
        for option in options:
179
            keypath, sep, val = option.partition('=')
180
            if not sep:
181
                raiseCLIError(
182
                    CLISyntaxError('Argument Syntax Error '),
183
                    details=[
184
                        '%s is missing a "="',
185
                        ' (usage: -o section.key=val)' % option])
186
            section, sep, key = keypath.partition('.')
187
        if not sep:
188
            key = section
189
            section = 'global'
190
        self._config_arg.value.override(
191
            section.strip(),
192
            key.strip(),
193
            val.strip())
194

    
195

    
196
class FlagArgument(Argument):
197
    """
198
    :value: true if set, false otherwise
199
    """
200

    
201
    def __init__(self, help='', parsed_name=None, default=False):
202
        super(FlagArgument, self).__init__(0, help, parsed_name, default)
203

    
204

    
205
class ValueArgument(Argument):
206
    """
207
    :value type: string
208
    :value returns: given value or default
209
    """
210

    
211
    def __init__(self, help='', parsed_name=None, default=None):
212
        super(ValueArgument, self).__init__(1, help, parsed_name, default)
213

    
214

    
215
class CommaSeparatedListArgument(ValueArgument):
216
    """
217
    :value type: string
218
    :value returns: list of the comma separated values
219
    """
220

    
221
    @property
222
    def value(self):
223
        return self._value or list()
224

    
225
    @value.setter
226
    def value(self, newvalue):
227
        self._value = newvalue.split(',') if newvalue else list()
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
            log.warning('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
    cloud=ValueArgument('Chose a cloud to connect to', ('--cloud')),
397
    help=Argument(0, 'Show help message', ('-h', '--help')),
398
    debug=FlagArgument('Include debug output', ('-d', '--debug')),
399
    include=FlagArgument(
400
        'Include raw connection data in the output', ('-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, 'Override a config value', ('-o', '--options'))
406
)
407

    
408

    
409
#  Initial command line interface arguments
410

    
411

    
412
class ArgumentParseManager(object):
413
    """Manage (initialize and update) an ArgumentParser object"""
414

    
415
    parser = None
416
    _arguments = {}
417
    _parser_modified = False
418
    _parsed = None
419
    _unparsed = None
420

    
421
    def __init__(self, exe, arguments=None):
422
        """
423
        :param exe: (str) the basic command (e.g. 'kamaki')
424

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

    
438
    @property
439
    def syntax(self):
440
        """The command syntax (useful for help messages, descriptions, etc)"""
441
        return self.parser.prog
442

    
443
    @syntax.setter
444
    def syntax(self, new_syntax):
445
        self.parser.prog = new_syntax
446

    
447
    @property
448
    def arguments(self):
449
        """(dict) arguments the parser should be aware of"""
450
        return self._arguments
451

    
452
    @arguments.setter
453
    def arguments(self, new_arguments):
454
        if new_arguments:
455
            assert isinstance(new_arguments, dict)
456
        self._arguments = new_arguments
457
        self.update_parser()
458

    
459
    @property
460
    def parsed(self):
461
        """(Namespace) parser-matched terms"""
462
        if self._parser_modified:
463
            self.parse()
464
        return self._parsed
465

    
466
    @property
467
    def unparsed(self):
468
        """(list) parser-unmatched terms"""
469
        if self._parser_modified:
470
            self.parse()
471
        return self._unparsed
472

    
473
    def update_parser(self, arguments=None):
474
        """Load argument specifications to parser
475

476
        :param arguments: if not given, update self.arguments instead
477
        """
478
        if not arguments:
479
            arguments = self._arguments
480

    
481
        for name, arg in arguments.items():
482
            try:
483
                arg.update_parser(self.parser, name)
484
                self._parser_modified = True
485
            except ArgumentError:
486
                pass
487

    
488
    def update_arguments(self, new_arguments):
489
        """Add to / update existing arguments
490

491
        :param new_arguments: (dict)
492
        """
493
        if new_arguments:
494
            assert isinstance(new_arguments, dict)
495
            self._arguments.update(new_arguments)
496
            self.update_parser()
497

    
498
    def parse(self, new_args=None):
499
        """Parse user input"""
500
        try:
501
            pkargs = (new_args,) if new_args else ()
502
            self._parsed, unparsed = self.parser.parse_known_args(*pkargs)
503
        except SystemExit:
504
            raiseCLIError(CLISyntaxError('Argument Syntax Error'))
505
        for name, arg in self.arguments.items():
506
            arg.value = getattr(self._parsed, name, arg.default)
507
        self._unparsed = []
508
        for term in unparsed:
509
            self._unparsed += split_input(' \'%s\' ' % term)
510
        self._parser_modified = False