Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument / __init__.py @ 1bd4f765

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
        """A key=val option"""
157
        return super(self.__class__, self).value
158

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

    
182

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

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

    
191

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

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

    
201

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

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

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

    
216

    
217
class IntArgument(ValueArgument):
218

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

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

    
236

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

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

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

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

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

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

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

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

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

    
283

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

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

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

    
299

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

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

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

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

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

    
328

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

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

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

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

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

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

    
376

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

    
391

    
392
#  Initial command line interface arguments
393

    
394

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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