Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument / __init__.py @ 8d427cb9

History | View | Annotate | Download (14.8 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
        try:
226
            self._value = self.default if (
227
                newvalue == self.default) else int(newvalue)
228
        except ValueError:
229
            raiseCLIError(CLISyntaxError(
230
                'IntArgument Error',
231
                details=['Value %s not an int' % newvalue]))
232

    
233

    
234
class DateArgument(ValueArgument):
235

    
236
    DATE_FORMAT = '%a %b %d %H:%M:%S %Y'
237

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

    
240
    @property
241
    def timestamp(self):
242
        v = getattr(self, '_value', self.default)
243
        return mktime(v.timetuple()) if v else None
244

    
245
    @property
246
    def formated(self):
247
        v = getattr(self, '_value', self.default)
248
        return v.strftime(self.DATE_FORMAT) if v else None
249

    
250
    @property
251
    def value(self):
252
        return self.timestamp
253

    
254
    @value.setter
255
    def value(self, newvalue):
256
        self._value = self.format_date(newvalue) if newvalue else self.default
257

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

    
269

    
270
class VersionArgument(FlagArgument):
271
    """A flag argument with that prints current version"""
272

    
273
    @property
274
    def value(self):
275
        """bool"""
276
        return super(self.__class__, self).value
277

    
278
    @value.setter
279
    def value(self, newvalue):
280
        self._value = newvalue
281
        if newvalue:
282
            import kamaki
283
            print('kamaki %s' % kamaki.__version__)
284

    
285

    
286
class KeyValueArgument(Argument):
287
    """A Value Argument that can be repeated
288

289
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
290
    """
291

    
292
    def __init__(self, help='', parsed_name=None, default={}):
293
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
294

    
295
    @property
296
    def value(self):
297
        """
298
        :returns: (dict) {key1: val1, key2: val2, ...}
299
        """
300
        return super(KeyValueArgument, self).value
301

    
302
    @value.setter
303
    def value(self, keyvalue_pairs):
304
        """
305
        :param keyvalue_pairs: (str) ['key1=val1', 'key2=val2', ...]
306
        """
307
        self._value = {}
308
        for pair in keyvalue_pairs:
309
            key, sep, val = pair.partition('=')
310
            if not sep:
311
                raiseCLIError(
312
                    CLISyntaxError('Argument syntax error '),
313
                    details='%s is missing a "=" (usage: key1=val1 )\n' % pair)
314
            self._value[key] = val
315

    
316

    
317
class ProgressBarArgument(FlagArgument):
318
    """Manage a progress bar"""
319

    
320
    def __init__(self, help='', parsed_name='', default=True):
321
        self.suffix = '%(percent)d%%'
322
        super(ProgressBarArgument, self).__init__(help, parsed_name, default)
323
        try:
324
            KamakiProgressBar
325
        except NameError:
326
            log.warning('WARNING: no progress bar functionality')
327

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

    
337
    def get_generator(self, message, message_len=25):
338
        """Get a generator to handle progress of the bar (gen.next())"""
339
        if self.value:
340
            return None
341
        try:
342
            self.bar = KamakiProgressBar()
343
        except NameError:
344
            self.value = None
345
            return self.value
346
        self.bar.message = message.ljust(message_len)
347
        self.bar.suffix = '%(percent)d%% - %(eta)ds'
348
        self.bar.start()
349

    
350
        def progress_gen(n):
351
            for i in self.bar.iter(range(int(n))):
352
                yield
353
            yield
354
        return progress_gen
355

    
356
    def finish(self):
357
        """Stop progress bar, return terminal cursor to user"""
358
        if self.value:
359
            return
360
        mybar = getattr(self, 'bar', None)
361
        if mybar:
362
            mybar.finish()
363

    
364

    
365
_arguments = dict(
366
    config=_config_arg,
367
    cloud=ValueArgument('Chose a cloud to connect to', ('--cloud')),
368
    help=Argument(0, 'Show help message', ('-h', '--help')),
369
    debug=FlagArgument('Include debug output', ('-d', '--debug')),
370
    include=FlagArgument(
371
        'Include raw connection data in the output', ('-i', '--include')),
372
    silent=FlagArgument('Do not output anything', ('-s', '--silent')),
373
    verbose=FlagArgument('More info at response', ('-v', '--verbose')),
374
    version=VersionArgument('Print current version', ('-V', '--version')),
375
    options=RuntimeConfigArgument(
376
        _config_arg, 'Override a config value', ('-o', '--options'))
377
)
378

    
379

    
380
#  Initial command line interface arguments
381

    
382

    
383
class ArgumentParseManager(object):
384
    """Manage (initialize and update) an ArgumentParser object"""
385

    
386
    parser = None
387
    _arguments = {}
388
    _parser_modified = False
389
    _parsed = None
390
    _unparsed = None
391

    
392
    def __init__(self, exe, arguments=None):
393
        """
394
        :param exe: (str) the basic command (e.g. 'kamaki')
395

396
        :param arguments: (dict) if given, overrides the global _argument as
397
            the parsers arguments specification
398
        """
399
        self.parser = ArgumentParser(
400
            add_help=False, formatter_class=RawDescriptionHelpFormatter)
401
        self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
402
        if arguments:
403
            self.arguments = arguments
404
        else:
405
            global _arguments
406
            self.arguments = _arguments
407
        self.parse()
408

    
409
    @property
410
    def syntax(self):
411
        """The command syntax (useful for help messages, descriptions, etc)"""
412
        return self.parser.prog
413

    
414
    @syntax.setter
415
    def syntax(self, new_syntax):
416
        self.parser.prog = new_syntax
417

    
418
    @property
419
    def arguments(self):
420
        """(dict) arguments the parser should be aware of"""
421
        return self._arguments
422

    
423
    @arguments.setter
424
    def arguments(self, new_arguments):
425
        if new_arguments:
426
            assert isinstance(new_arguments, dict)
427
        self._arguments = new_arguments
428
        self.update_parser()
429

    
430
    @property
431
    def parsed(self):
432
        """(Namespace) parser-matched terms"""
433
        if self._parser_modified:
434
            self.parse()
435
        return self._parsed
436

    
437
    @property
438
    def unparsed(self):
439
        """(list) parser-unmatched terms"""
440
        if self._parser_modified:
441
            self.parse()
442
        return self._unparsed
443

    
444
    def update_parser(self, arguments=None):
445
        """Load argument specifications to parser
446

447
        :param arguments: if not given, update self.arguments instead
448
        """
449
        if not arguments:
450
            arguments = self._arguments
451

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

    
459
    def update_arguments(self, new_arguments):
460
        """Add to / update existing arguments
461

462
        :param new_arguments: (dict)
463
        """
464
        if new_arguments:
465
            assert isinstance(new_arguments, dict)
466
            self._arguments.update(new_arguments)
467
            self.update_parser()
468

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