Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument / __init__.py @ 34c480f2

History | View | Annotate | Download (15 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

    
68
        assert parsed_name, 'No parsed name for argument %s' % self
69
        self.parsed_name = list(parsed_name) if isinstance(
70
            parsed_name, list) or isinstance(parsed_name, tuple) else (
71
                '%s' % parsed_name).split()
72
        for name in self.parsed_name:
73
            assert name.count(' ') == 0, '%s: Invalid parse name "%s"' % (
74
                self, name)
75
            msg = '%s: Invalid parse name "%s" should start with a "-"' % (
76
                    self, name)
77
            assert name.startswith('-'), msg
78

    
79
        self.default = default or (None if self.arity else False)
80

    
81
    @property
82
    def value(self):
83
        return getattr(self, '_value', self.default)
84

    
85
    @value.setter
86
    def value(self, newvalue):
87
        self._value = newvalue
88

    
89
    def update_parser(self, parser, name):
90
        """Update argument parser with self info"""
91
        action = 'append' if self.arity < 0 else (
92
            'store' if self.arity else 'store_true')
93
        parser.add_argument(
94
            *self.parsed_name,
95
            dest=name, action=action, default=self.default, help=self.help)
96

    
97

    
98
class ConfigArgument(Argument):
99
    """Manage a kamaki configuration (file)"""
100

    
101
    def __init__(self, help, parsed_name=('-c', '--config')):
102
        super(ConfigArgument, self).__init__(1, help, parsed_name, None)
103
        self.file_path = None
104

    
105
    @property
106
    def value(self):
107
        return super(ConfigArgument, self).value
108

    
109
    @value.setter
110
    def value(self, config_file):
111
        if config_file:
112
            self._value = Config(config_file)
113
            self.file_path = config_file
114
        elif self.file_path:
115
            self._value = Config(self.file_path)
116
        else:
117
            self._value = Config()
118

    
119
    def get(self, group, term):
120
        """Get a configuration setting from the Config object"""
121
        return self.value.get(group, term)
122

    
123
    @property
124
    def groups(self):
125
        suffix = '_cli'
126
        slen = len(suffix)
127
        return [term[:-slen] for term in self.value.keys('global') if (
128
            term.endswith(suffix))]
129

    
130
    @property
131
    def cli_specs(self):
132
        suffix = '_cli'
133
        slen = len(suffix)
134
        return [(k[:-slen], v) for k, v in self.value.items('global') if (
135
            k.endswith(suffix))]
136

    
137
    def get_global(self, option):
138
        return self.value.get_global(option)
139

    
140
    def get_cloud(self, cloud, option):
141
        return self.value.get_cloud(cloud, option)
142

    
143

    
144
_config_arg = ConfigArgument('Path to config file')
145

    
146

    
147
class RuntimeConfigArgument(Argument):
148
    """Set a run-time setting option (not persistent)"""
149

    
150
    def __init__(self, config_arg, help='', parsed_name=None, default=None):
151
        super(self.__class__, self).__init__(1, help, parsed_name, default)
152
        self._config_arg = config_arg
153

    
154
    @property
155
    def value(self):
156
        return super(RuntimeConfigArgument, self).value
157

    
158
    @value.setter
159
    def value(self, options):
160
        if options == self.default:
161
            return
162
        if not isinstance(options, list):
163
            options = ['%s' % options]
164
        for option in options:
165
            keypath, sep, val = option.partition('=')
166
            if not sep:
167
                raiseCLIError(
168
                    CLISyntaxError('Argument Syntax Error '),
169
                    details=[
170
                        '%s is missing a "="',
171
                        ' (usage: -o section.key=val)' % option])
172
            section, sep, key = keypath.partition('.')
173
        if not sep:
174
            key = section
175
            section = 'global'
176
        self._config_arg.value.override(
177
            section.strip(),
178
            key.strip(),
179
            val.strip())
180

    
181

    
182
class FlagArgument(Argument):
183
    """
184
    :value: true if set, false otherwise
185
    """
186

    
187
    def __init__(self, help='', parsed_name=None, default=False):
188
        super(FlagArgument, self).__init__(0, help, parsed_name, default)
189

    
190

    
191
class ValueArgument(Argument):
192
    """
193
    :value type: string
194
    :value returns: given value or default
195
    """
196

    
197
    def __init__(self, help='', parsed_name=None, default=None):
198
        super(ValueArgument, self).__init__(1, help, parsed_name, default)
199

    
200

    
201
class CommaSeparatedListArgument(ValueArgument):
202
    """
203
    :value type: string
204
    :value returns: list of the comma separated values
205
    """
206

    
207
    @property
208
    def value(self):
209
        return self._value or list()
210

    
211
    @value.setter
212
    def value(self, newvalue):
213
        self._value = newvalue.split(',') if newvalue else list()
214

    
215

    
216
class IntArgument(ValueArgument):
217

    
218
    @property
219
    def value(self):
220
        """integer (type checking)"""
221
        return getattr(self, '_value', self.default)
222

    
223
    @value.setter
224
    def value(self, newvalue):
225
        if newvalue == self.default:
226
            self._value = self.default
227
            return
228
        try:
229
            self._value = int(newvalue)
230
        except ValueError:
231
            raiseCLIError(CLISyntaxError(
232
                'IntArgument Error',
233
                details=['Value %s not an int' % newvalue]))
234

    
235

    
236
class DateArgument(ValueArgument):
237
    """
238
    :value type: a string formated in an acceptable date format
239

240
    :value returns: same date in first of DATE_FORMATS
241
    """
242

    
243
    DATE_FORMATS = [
244
        "%a %b %d %H:%M:%S %Y",
245
        "%A, %d-%b-%y %H:%M:%S GMT",
246
        "%a, %d %b %Y %H:%M:%S GMT"]
247

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

    
250
    @property
251
    def timestamp(self):
252
        v = getattr(self, '_value', self.default)
253
        return mktime(v.timetuple()) if v else None
254

    
255
    @property
256
    def formated(self):
257
        v = getattr(self, '_value', self.default)
258
        return v.strftime(self.DATE_FORMATS[0]) if v else None
259

    
260
    @property
261
    def value(self):
262
        return self.timestamp
263

    
264
    @value.setter
265
    def value(self, newvalue):
266
        if newvalue:
267
            self._value = self.format_date(newvalue)
268

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

    
282

    
283
class VersionArgument(FlagArgument):
284
    """A flag argument with that prints current version"""
285

    
286
    @property
287
    def value(self):
288
        """bool"""
289
        return super(self.__class__, self).value
290

    
291
    @value.setter
292
    def value(self, newvalue):
293
        self._value = newvalue
294
        if newvalue:
295
            import kamaki
296
            print('kamaki %s' % kamaki.__version__)
297

    
298

    
299
class KeyValueArgument(Argument):
300
    """A Value Argument that can be repeated
301

302
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
303
    """
304

    
305
    def __init__(self, help='', parsed_name=None, default=[]):
306
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
307

    
308
    @property
309
    def value(self):
310
        """
311
        :input: key=value
312
        :output: {'key1':'value1', 'key2':'value2', ...}
313
        """
314
        return super(KeyValueArgument, self).value
315

    
316
    @value.setter
317
    def value(self, keyvalue_pairs):
318
        self._value = {}
319
        for pair in keyvalue_pairs:
320
            key, sep, val = pair.partition('=')
321
            if not sep:
322
                raiseCLIError(
323
                    CLISyntaxError('Argument syntax error '),
324
                    details='%s is missing a "=" (usage: key1=val1 )\n' % pair)
325
            self._value[key.strip()] = val.strip()
326

    
327

    
328
class ProgressBarArgument(FlagArgument):
329
    """Manage a progress bar"""
330

    
331
    def __init__(self, help='', parsed_name='', default=True):
332
        self.suffix = '%(percent)d%%'
333
        super(ProgressBarArgument, self).__init__(help, parsed_name, default)
334
        try:
335
            KamakiProgressBar
336
        except NameError:
337
            log.warning('WARNING: no progress bar functionality')
338

    
339
    def clone(self):
340
        """Get a modifiable copy of this bar"""
341
        newarg = ProgressBarArgument(
342
            self.help,
343
            self.parsed_name,
344
            self.default)
345
        newarg._value = self._value
346
        return newarg
347

    
348
    def get_generator(self, message, message_len=25):
349
        """Get a generator to handle progress of the bar (gen.next())"""
350
        if self.value:
351
            return None
352
        try:
353
            self.bar = KamakiProgressBar()
354
        except NameError:
355
            self.value = None
356
            return self.value
357
        self.bar.message = message.ljust(message_len)
358
        self.bar.suffix = '%(percent)d%% - %(eta)ds'
359
        self.bar.start()
360

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

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

    
375

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

    
390

    
391
#  Initial command line interface arguments
392

    
393

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

    
397
    parser = None
398
    _arguments = {}
399
    _parser_modified = False
400
    _parsed = None
401
    _unparsed = None
402

    
403
    def __init__(self, exe, arguments=None):
404
        """
405
        :param exe: (str) the basic command (e.g. 'kamaki')
406

407
        :param arguments: (dict) if given, overrides the global _argument as
408
            the parsers arguments specification
409
        """
410
        self.parser = ArgumentParser(
411
            add_help=False, formatter_class=RawDescriptionHelpFormatter)
412
        self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
413
        if arguments:
414
            self.arguments = arguments
415
        else:
416
            global _arguments
417
            self.arguments = _arguments
418
        self.parse()
419

    
420
    @property
421
    def syntax(self):
422
        """The command syntax (useful for help messages, descriptions, etc)"""
423
        return self.parser.prog
424

    
425
    @syntax.setter
426
    def syntax(self, new_syntax):
427
        self.parser.prog = new_syntax
428

    
429
    @property
430
    def arguments(self):
431
        """(dict) arguments the parser should be aware of"""
432
        return self._arguments
433

    
434
    @arguments.setter
435
    def arguments(self, new_arguments):
436
        if new_arguments:
437
            assert isinstance(new_arguments, dict)
438
        self._arguments = new_arguments
439
        self.update_parser()
440

    
441
    @property
442
    def parsed(self):
443
        """(Namespace) parser-matched terms"""
444
        if self._parser_modified:
445
            self.parse()
446
        return self._parsed
447

    
448
    @property
449
    def unparsed(self):
450
        """(list) parser-unmatched terms"""
451
        if self._parser_modified:
452
            self.parse()
453
        return self._unparsed
454

    
455
    def update_parser(self, arguments=None):
456
        """Load argument specifications to parser
457

458
        :param arguments: if not given, update self.arguments instead
459
        """
460
        if not arguments:
461
            arguments = self._arguments
462

    
463
        for name, arg in arguments.items():
464
            try:
465
                arg.update_parser(self.parser, name)
466
                self._parser_modified = True
467
            except ArgumentError:
468
                pass
469

    
470
    def update_arguments(self, new_arguments):
471
        """Add to / update existing arguments
472

473
        :param new_arguments: (dict)
474
        """
475
        if new_arguments:
476
            assert isinstance(new_arguments, dict)
477
            self._arguments.update(new_arguments)
478
            self.update_parser()
479

    
480
    def parse(self, new_args=None):
481
        """Parse user input"""
482
        try:
483
            pkargs = (new_args,) if new_args else ()
484
            self._parsed, unparsed = self.parser.parse_known_args(*pkargs)
485
        except SystemExit:
486
            raiseCLIError(CLISyntaxError('Argument Syntax Error'))
487
        for name, arg in self.arguments.items():
488
            arg.value = getattr(self._parsed, name, arg.default)
489
        self._unparsed = []
490
        for term in unparsed:
491
            self._unparsed += split_input(' \'%s\' ' % term)
492
        self._parser_modified = False