Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument / __init__.py @ 02987e9c

History | View | Annotate | Download (14.9 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 = int(newvalue)
227
        except ValueError:
228
            raiseCLIError(CLISyntaxError(
229
                'IntArgument Error',
230
                details=['Value %s not an int' % newvalue]))
231

    
232

    
233
class DateArgument(ValueArgument):
234
    """
235
    :value type: a string formated in an acceptable date format
236

237
    :value returns: same date in first of DATE_FORMATS
238
    """
239

    
240
    DATE_FORMATS = [
241
        "%a %b %d %H:%M:%S %Y",
242
        "%A, %d-%b-%y %H:%M:%S GMT",
243
        "%a, %d %b %Y %H:%M:%S GMT"]
244

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

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

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

    
257
    @property
258
    def value(self):
259
        return self.timestamp
260

    
261
    @value.setter
262
    def value(self, newvalue):
263
        if newvalue:
264
            self._value = self.format_date(newvalue)
265

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

    
279

    
280
class VersionArgument(FlagArgument):
281
    """A flag argument with that prints current version"""
282

    
283
    @property
284
    def value(self):
285
        """bool"""
286
        return super(self.__class__, self).value
287

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

    
295

    
296
class KeyValueArgument(Argument):
297
    """A Value Argument that can be repeated
298

299
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
300
    """
301

    
302
    def __init__(self, help='', parsed_name=None, default=[]):
303
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
304

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

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

    
324

    
325
class ProgressBarArgument(FlagArgument):
326
    """Manage a progress bar"""
327

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

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

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

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

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

    
372

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

    
387

    
388
#  Initial command line interface arguments
389

    
390

    
391
class ArgumentParseManager(object):
392
    """Manage (initialize and update) an ArgumentParser object"""
393

    
394
    parser = None
395
    _arguments = {}
396
    _parser_modified = False
397
    _parsed = None
398
    _unparsed = None
399

    
400
    def __init__(self, exe, arguments=None):
401
        """
402
        :param exe: (str) the basic command (e.g. 'kamaki')
403

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

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

    
422
    @syntax.setter
423
    def syntax(self, new_syntax):
424
        self.parser.prog = new_syntax
425

    
426
    @property
427
    def arguments(self):
428
        """(dict) arguments the parser should be aware of"""
429
        return self._arguments
430

    
431
    @arguments.setter
432
    def arguments(self, new_arguments):
433
        if new_arguments:
434
            assert isinstance(new_arguments, dict)
435
        self._arguments = new_arguments
436
        self.update_parser()
437

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

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

    
452
    def update_parser(self, arguments=None):
453
        """Load argument specifications to parser
454

455
        :param arguments: if not given, update self.arguments instead
456
        """
457
        if not arguments:
458
            arguments = self._arguments
459

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

    
467
    def update_arguments(self, new_arguments):
468
        """Add to / update existing arguments
469

470
        :param new_arguments: (dict)
471
        """
472
        if new_arguments:
473
            assert isinstance(new_arguments, dict)
474
            self._arguments.update(new_arguments)
475
            self.update_parser()
476

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