Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument / __init__.py @ 534e7bbb

History | View | Annotate | Download (15.3 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
from progress.bar import ShadyBar as KamakiProgressBar
45

    
46
log = getLogger(__name__)
47

    
48

    
49
class Argument(object):
50
    """An argument that can be parsed from command line or otherwise.
51
    This is the top-level Argument class. It is suggested to extent this
52
    class into more specific argument types.
53
    """
54

    
55
    def __init__(self, arity, help=None, parsed_name=None, default=None):
56
        self.arity = int(arity)
57
        self.help = '%s' % help or ''
58

    
59
        assert parsed_name, 'No parsed name for argument %s' % self
60
        self.parsed_name = list(parsed_name) if isinstance(
61
            parsed_name, list) or isinstance(parsed_name, tuple) else (
62
                '%s' % parsed_name).split()
63
        for name in self.parsed_name:
64
            assert name.count(' ') == 0, '%s: Invalid parse name "%s"' % (
65
                self, name)
66
            msg = '%s: Invalid parse name "%s" should start with a "-"' % (
67
                    self, name)
68
            assert name.startswith('-'), msg
69

    
70
        self.default = default if (default or self.arity) else False
71

    
72
    @property
73
    def value(self):
74
        return getattr(self, '_value', self.default)
75

    
76
    @value.setter
77
    def value(self, newvalue):
78
        self._value = newvalue
79

    
80
    def update_parser(self, parser, name):
81
        """Update argument parser with self info"""
82
        action = 'append' if self.arity < 0 else (
83
            'store' if self.arity else 'store_true')
84
        parser.add_argument(
85
            *self.parsed_name,
86
            dest=name, action=action, default=self.default, help=self.help)
87

    
88

    
89
class ConfigArgument(Argument):
90
    """Manage a kamaki configuration (file)"""
91

    
92
    def __init__(self, help, parsed_name=('-c', '--config')):
93
        super(ConfigArgument, self).__init__(1, help, parsed_name, None)
94
        self.file_path = None
95

    
96
    @property
97
    def value(self):
98
        return super(ConfigArgument, self).value
99

    
100
    @value.setter
101
    def value(self, config_file):
102
        if config_file:
103
            self._value = Config(config_file)
104
            self.file_path = config_file
105
        elif self.file_path:
106
            self._value = Config(self.file_path)
107
        else:
108
            self._value = Config()
109

    
110
    def get(self, group, term):
111
        """Get a configuration setting from the Config object"""
112
        return self.value.get(group, term)
113

    
114
    @property
115
    def groups(self):
116
        suffix = '_cli'
117
        slen = len(suffix)
118
        return [term[:-slen] for term in self.value.keys('global') if (
119
            term.endswith(suffix))]
120

    
121
    @property
122
    def cli_specs(self):
123
        suffix = '_cli'
124
        slen = len(suffix)
125
        return [(k[:-slen], v) for k, v in self.value.items('global') if (
126
            k.endswith(suffix))]
127

    
128
    def get_global(self, option):
129
        return self.value.get('global', option)
130

    
131
    def get_cloud(self, cloud, option):
132
        return self.value.get_cloud(cloud, option)
133

    
134

    
135
_config_arg = ConfigArgument('Path to config file')
136

    
137

    
138
class RuntimeConfigArgument(Argument):
139
    """Set a run-time setting option (not persistent)"""
140

    
141
    def __init__(self, config_arg, help='', parsed_name=None, default=None):
142
        super(self.__class__, self).__init__(1, help, parsed_name, default)
143
        self._config_arg = config_arg
144

    
145
    @property
146
    def value(self):
147
        return super(RuntimeConfigArgument, self).value
148

    
149
    @value.setter
150
    def value(self, options):
151
        if options == self.default:
152
            return
153
        if not isinstance(options, list):
154
            options = ['%s' % options]
155
        for option in options:
156
            keypath, sep, val = option.partition('=')
157
            if not sep:
158
                raiseCLIError(
159
                    CLISyntaxError('Argument Syntax Error '),
160
                    details=[
161
                        '%s is missing a "="',
162
                        ' (usage: -o section.key=val)' % option])
163
            section, sep, key = keypath.partition('.')
164
        if not sep:
165
            key = section
166
            section = 'global'
167
        self._config_arg.value.override(
168
            section.strip(),
169
            key.strip(),
170
            val.strip())
171

    
172

    
173
class FlagArgument(Argument):
174
    """
175
    :value: true if set, false otherwise
176
    """
177

    
178
    def __init__(self, help='', parsed_name=None, default=False):
179
        super(FlagArgument, self).__init__(0, help, parsed_name, default)
180

    
181

    
182
class ValueArgument(Argument):
183
    """
184
    :value type: string
185
    :value returns: given value or default
186
    """
187

    
188
    def __init__(self, help='', parsed_name=None, default=None):
189
        super(ValueArgument, self).__init__(1, help, parsed_name, default)
190

    
191

    
192
class CommaSeparatedListArgument(ValueArgument):
193
    """
194
    :value type: string
195
    :value returns: list of the comma separated values
196
    """
197

    
198
    @property
199
    def value(self):
200
        return self._value or list()
201

    
202
    @value.setter
203
    def value(self, newvalue):
204
        self._value = newvalue.split(',') if newvalue else list()
205

    
206

    
207
class IntArgument(ValueArgument):
208

    
209
    @property
210
    def value(self):
211
        """integer (type checking)"""
212
        return getattr(self, '_value', self.default)
213

    
214
    @value.setter
215
    def value(self, newvalue):
216
        if newvalue == self.default:
217
            self._value = newvalue
218
            return
219
        try:
220
            if int(newvalue) == float(newvalue):
221
                self._value = int(newvalue)
222
            else:
223
                raise ValueError('Raise int argument error')
224
        except ValueError:
225
            raiseCLIError(CLISyntaxError(
226
                'IntArgument Error',
227
                details=['Value %s not an int' % newvalue]))
228

    
229

    
230
class DateArgument(ValueArgument):
231

    
232
    DATE_FORMAT = '%a %b %d %H:%M:%S %Y'
233

    
234
    INPUT_FORMATS = [DATE_FORMAT, '%d-%m-%Y', '%H:%M:%S %d-%m-%Y']
235

    
236
    @property
237
    def timestamp(self):
238
        v = getattr(self, '_value', self.default)
239
        return mktime(v.timetuple()) if v else None
240

    
241
    @property
242
    def formated(self):
243
        v = getattr(self, '_value', self.default)
244
        return v.strftime(self.DATE_FORMAT) if v else None
245

    
246
    @property
247
    def value(self):
248
        return self.timestamp
249

    
250
    @value.setter
251
    def value(self, newvalue):
252
        self._value = self.format_date(newvalue) if newvalue else self.default
253

    
254
    def format_date(self, datestr):
255
        for format in self.INPUT_FORMATS:
256
            try:
257
                t = dtm.strptime(datestr, format)
258
            except ValueError:
259
                continue
260
            return t  # .strftime(self.DATE_FORMAT)
261
        raiseCLIError(None, 'Date Argument Error', details=[
262
            '%s not a valid date' % datestr,
263
            'Correct formats:\n\t%s' % self.INPUT_FORMATS])
264

    
265

    
266
class VersionArgument(FlagArgument):
267
    """A flag argument with that prints current version"""
268

    
269
    @property
270
    def value(self):
271
        """bool"""
272
        return super(self.__class__, self).value
273

    
274
    @value.setter
275
    def value(self, newvalue):
276
        self._value = newvalue
277
        if newvalue:
278
            import kamaki
279
            print('kamaki %s' % kamaki.__version__)
280

    
281

    
282
class RepeatableArgument(Argument):
283
    """A value argument that can be repeated"""
284

    
285
    def __init__(self, help='', parsed_name=None, default=[]):
286
        super(RepeatableArgument, self).__init__(
287
            -1, help, parsed_name, default)
288

    
289

    
290
class KeyValueArgument(Argument):
291
    """A Key=Value Argument that can be repeated
292

293
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
294
    """
295

    
296
    def __init__(self, help='', parsed_name=None, default=[]):
297
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
298

    
299
    @property
300
    def value(self):
301
        """
302
        :returns: (dict) {key1: val1, key2: val2, ...}
303
        """
304
        return super(KeyValueArgument, self).value
305

    
306
    @value.setter
307
    def value(self, keyvalue_pairs):
308
        """
309
        :param keyvalue_pairs: (str) ['key1=val1', 'key2=val2', ...]
310
        """
311
        self._value = getattr(self, '_value', self.value) or {}
312
        try:
313
            for pair in keyvalue_pairs:
314
                key, sep, val = pair.partition('=')
315
                assert sep, ' %s misses a "=" (usage: key1=val1 )\n' % (pair)
316
                self._value[key] = val
317
        except Exception as e:
318
            raiseCLIError(e, 'KeyValueArgument Syntax Error')
319

    
320

    
321
class ProgressBarArgument(FlagArgument):
322
    """Manage a progress bar"""
323

    
324
    def __init__(self, help='', parsed_name='', default=True):
325
        self.suffix = '%(percent)d%%'
326
        super(ProgressBarArgument, self).__init__(help, parsed_name, default)
327

    
328
    def clone(self):
329
        """Get a modifiable copy of this bar"""
330
        newarg = ProgressBarArgument(
331
            self.help, self.parsed_name, self.default)
332
        newarg._value = self._value
333
        return newarg
334

    
335
    def get_generator(
336
            self, message, message_len=25, countdown=False, timeout=100):
337
        """Get a generator to handle progress of the bar (gen.next())"""
338
        if self.value:
339
            return None
340
        try:
341
            self.bar = KamakiProgressBar()
342
        except NameError:
343
            self.value = None
344
            return self.value
345
        if countdown:
346
            bar_phases = list(self.bar.phases)
347
            self.bar.empty_fill, bar_phases[0] = bar_phases[-1], ''
348
            bar_phases.reverse()
349
            self.bar.phases = bar_phases
350
            self.bar.bar_prefix = ' '
351
            self.bar.bar_suffix = ' '
352
            self.bar.max = timeout or 100
353
            self.bar.suffix = '%(remaining)ds to timeout'
354
        else:
355
            self.bar.suffix = '%(percent)d%% - %(eta)ds'
356
        self.bar.eta = timeout or 100
357
        self.bar.message = message.ljust(message_len)
358
        self.bar.start()
359

    
360
        def progress_gen(n):
361
            for i in self.bar.iter(range(int(n))):
362
                yield
363
            yield
364
        return progress_gen
365

    
366
    def finish(self):
367
        """Stop progress bar, return terminal cursor to user"""
368
        if self.value:
369
            return
370
        mybar = getattr(self, 'bar', None)
371
        if mybar:
372
            mybar.finish()
373

    
374

    
375
_arguments = dict(
376
    config=_config_arg,
377
    cloud=ValueArgument('Chose a cloud to connect to', ('--cloud')),
378
    help=Argument(0, 'Show help message', ('-h', '--help')),
379
    debug=FlagArgument('Include debug output', ('-d', '--debug')),
380
    #include=FlagArgument(
381
    #    'Include raw connection data in the output', ('-i', '--include')),
382
    silent=FlagArgument('Do not output anything', ('-s', '--silent')),
383
    verbose=FlagArgument('More info at response', ('-v', '--verbose')),
384
    version=VersionArgument('Print current version', ('-V', '--version')),
385
    options=RuntimeConfigArgument(
386
        _config_arg, 'Override a config value', ('-o', '--options'))
387
)
388

    
389

    
390
#  Initial command line interface arguments
391

    
392

    
393
class ArgumentParseManager(object):
394
    """Manage (initialize and update) an ArgumentParser object"""
395

    
396
    def __init__(self, exe, arguments=None):
397
        """
398
        :param exe: (str) the basic command (e.g. 'kamaki')
399

400
        :param arguments: (dict) if given, overrides the global _argument as
401
            the parsers arguments specification
402
        """
403
        self.parser = ArgumentParser(
404
            add_help=False, formatter_class=RawDescriptionHelpFormatter)
405
        self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
406
        if arguments:
407
            self.arguments = arguments
408
        else:
409
            global _arguments
410
            self.arguments = _arguments
411
        self._parser_modified, self._parsed, self._unparsed = False, None, None
412
        self.parse()
413

    
414
    @property
415
    def syntax(self):
416
        """The command syntax (useful for help messages, descriptions, etc)"""
417
        return self.parser.prog
418

    
419
    @syntax.setter
420
    def syntax(self, new_syntax):
421
        self.parser.prog = new_syntax
422

    
423
    @property
424
    def arguments(self):
425
        """:returns: (dict) arguments the parser should be aware of"""
426
        return self._arguments
427

    
428
    @arguments.setter
429
    def arguments(self, new_arguments):
430
        assert isinstance(new_arguments, dict), 'Arguments must be in a dict'
431
        self._arguments = new_arguments
432
        self.update_parser()
433

    
434
    @property
435
    def parsed(self):
436
        """(Namespace) parser-matched terms"""
437
        if self._parser_modified:
438
            self.parse()
439
        return self._parsed
440

    
441
    @property
442
    def unparsed(self):
443
        """(list) parser-unmatched terms"""
444
        if self._parser_modified:
445
            self.parse()
446
        return self._unparsed
447

    
448
    def update_parser(self, arguments=None):
449
        """Load argument specifications to parser
450

451
        :param arguments: if not given, update self.arguments instead
452
        """
453
        arguments = arguments or self._arguments
454

    
455
        for name, arg in arguments.items():
456
            try:
457
                arg.update_parser(self.parser, name)
458
                self._parser_modified = True
459
            except ArgumentError:
460
                pass
461

    
462
    def update_arguments(self, new_arguments):
463
        """Add to / update existing arguments
464

465
        :param new_arguments: (dict)
466
        """
467
        if new_arguments:
468
            assert isinstance(new_arguments, dict)
469
            self._arguments.update(new_arguments)
470
            self.update_parser()
471

    
472
    def parse(self, new_args=None):
473
        """Parse user input"""
474
        try:
475
            pkargs = (new_args,) if new_args else ()
476
            self._parsed, unparsed = self.parser.parse_known_args(*pkargs)
477
        except SystemExit:
478
            raiseCLIError(CLISyntaxError('Argument Syntax Error'))
479
        for name, arg in self.arguments.items():
480
            arg.value = getattr(self._parsed, name, arg.default)
481
        self._unparsed = []
482
        for term in unparsed:
483
            self._unparsed += split_input(' \'%s\' ' % term)
484
        self._parser_modified = False