Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument / __init__.py @ 67083ca0

History | View | Annotate | Download (14.4 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 or (None if 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
        try:
217
            self._value = self.default if (
218
                newvalue == self.default) else int(newvalue)
219
        except ValueError:
220
            raiseCLIError(CLISyntaxError(
221
                'IntArgument Error',
222
                details=['Value %s not an int' % newvalue]))
223

    
224

    
225
class DateArgument(ValueArgument):
226

    
227
    DATE_FORMAT = '%a %b %d %H:%M:%S %Y'
228

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

    
231
    @property
232
    def timestamp(self):
233
        v = getattr(self, '_value', self.default)
234
        return mktime(v.timetuple()) if v else None
235

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

    
241
    @property
242
    def value(self):
243
        return self.timestamp
244

    
245
    @value.setter
246
    def value(self, newvalue):
247
        self._value = self.format_date(newvalue) if newvalue else self.default
248

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

    
260

    
261
class VersionArgument(FlagArgument):
262
    """A flag argument with that prints current version"""
263

    
264
    @property
265
    def value(self):
266
        """bool"""
267
        return super(self.__class__, self).value
268

    
269
    @value.setter
270
    def value(self, newvalue):
271
        self._value = newvalue
272
        if newvalue:
273
            import kamaki
274
            print('kamaki %s' % kamaki.__version__)
275

    
276

    
277
class KeyValueArgument(Argument):
278
    """A Value Argument that can be repeated
279

280
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
281
    """
282

    
283
    def __init__(self, help='', parsed_name=None, default={}):
284
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
285

    
286
    @property
287
    def value(self):
288
        """
289
        :returns: (dict) {key1: val1, key2: val2, ...}
290
        """
291
        return super(KeyValueArgument, self).value
292

    
293
    @value.setter
294
    def value(self, keyvalue_pairs):
295
        """
296
        :param keyvalue_pairs: (str) ['key1=val1', 'key2=val2', ...]
297
        """
298
        self._value = {}
299
        for pair in keyvalue_pairs:
300
            key, sep, val = pair.partition('=')
301
            if not sep:
302
                raiseCLIError(
303
                    CLISyntaxError('Argument syntax error '),
304
                    details='%s is missing a "=" (usage: key1=val1 )\n' % pair)
305
            self._value[key] = val
306

    
307

    
308
class ProgressBarArgument(FlagArgument):
309
    """Manage a progress bar"""
310

    
311
    def __init__(self, help='', parsed_name='', default=True):
312
        self.suffix = '%(percent)d%%'
313
        super(ProgressBarArgument, self).__init__(help, parsed_name, default)
314

    
315
    def clone(self):
316
        """Get a modifiable copy of this bar"""
317
        newarg = ProgressBarArgument(
318
            self.help,
319
            self.parsed_name,
320
            self.default)
321
        newarg._value = self._value
322
        return newarg
323

    
324
    def get_generator(self, message, message_len=25):
325
        """Get a generator to handle progress of the bar (gen.next())"""
326
        if self.value:
327
            return None
328
        try:
329
            self.bar = KamakiProgressBar()
330
        except NameError:
331
            self.value = None
332
            return self.value
333
        self.bar.message = message.ljust(message_len)
334
        self.bar.suffix = '%(percent)d%% - %(eta)ds'
335
        self.bar.start()
336

    
337
        def progress_gen(n):
338
            for i in self.bar.iter(range(int(n))):
339
                yield
340
            yield
341
        return progress_gen
342

    
343
    def finish(self):
344
        """Stop progress bar, return terminal cursor to user"""
345
        if self.value:
346
            return
347
        mybar = getattr(self, 'bar', None)
348
        if mybar:
349
            mybar.finish()
350

    
351

    
352
_arguments = dict(
353
    config=_config_arg,
354
    cloud=ValueArgument('Chose a cloud to connect to', ('--cloud')),
355
    help=Argument(0, 'Show help message', ('-h', '--help')),
356
    debug=FlagArgument('Include debug output', ('-d', '--debug')),
357
    include=FlagArgument(
358
        'Include raw connection data in the output', ('-i', '--include')),
359
    silent=FlagArgument('Do not output anything', ('-s', '--silent')),
360
    verbose=FlagArgument('More info at response', ('-v', '--verbose')),
361
    version=VersionArgument('Print current version', ('-V', '--version')),
362
    options=RuntimeConfigArgument(
363
        _config_arg, 'Override a config value', ('-o', '--options'))
364
)
365

    
366

    
367
#  Initial command line interface arguments
368

    
369

    
370
class ArgumentParseManager(object):
371
    """Manage (initialize and update) an ArgumentParser object"""
372

    
373
    parser = None
374
    _arguments = {}
375
    _parser_modified = False
376
    _parsed = None
377
    _unparsed = None
378

    
379
    def __init__(self, exe, arguments=None):
380
        """
381
        :param exe: (str) the basic command (e.g. 'kamaki')
382

383
        :param arguments: (dict) if given, overrides the global _argument as
384
            the parsers arguments specification
385
        """
386
        self.parser = ArgumentParser(
387
            add_help=False, formatter_class=RawDescriptionHelpFormatter)
388
        self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
389
        if arguments:
390
            self.arguments = arguments
391
        else:
392
            global _arguments
393
            self.arguments = _arguments
394
        self.parse()
395

    
396
    @property
397
    def syntax(self):
398
        """The command syntax (useful for help messages, descriptions, etc)"""
399
        return self.parser.prog
400

    
401
    @syntax.setter
402
    def syntax(self, new_syntax):
403
        self.parser.prog = new_syntax
404

    
405
    @property
406
    def arguments(self):
407
        """(dict) arguments the parser should be aware of"""
408
        return self._arguments
409

    
410
    @arguments.setter
411
    def arguments(self, new_arguments):
412
        if new_arguments:
413
            assert isinstance(new_arguments, dict)
414
        self._arguments = new_arguments
415
        self.update_parser()
416

    
417
    @property
418
    def parsed(self):
419
        """(Namespace) parser-matched terms"""
420
        if self._parser_modified:
421
            self.parse()
422
        return self._parsed
423

    
424
    @property
425
    def unparsed(self):
426
        """(list) parser-unmatched terms"""
427
        if self._parser_modified:
428
            self.parse()
429
        return self._unparsed
430

    
431
    def update_parser(self, arguments=None):
432
        """Load argument specifications to parser
433

434
        :param arguments: if not given, update self.arguments instead
435
        """
436
        if not arguments:
437
            arguments = self._arguments
438

    
439
        for name, arg in arguments.items():
440
            try:
441
                arg.update_parser(self.parser, name)
442
                self._parser_modified = True
443
            except ArgumentError:
444
                pass
445

    
446
    def update_arguments(self, new_arguments):
447
        """Add to / update existing arguments
448

449
        :param new_arguments: (dict)
450
        """
451
        if new_arguments:
452
            assert isinstance(new_arguments, dict)
453
            self._arguments.update(new_arguments)
454
            self.update_parser()
455

    
456
    def parse(self, new_args=None):
457
        """Parse user input"""
458
        try:
459
            pkargs = (new_args,) if new_args else ()
460
            self._parsed, unparsed = self.parser.parse_known_args(*pkargs)
461
        except SystemExit:
462
            raiseCLIError(CLISyntaxError('Argument Syntax Error'))
463
        for name, arg in self.arguments.items():
464
            arg.value = getattr(self._parsed, name, arg.default)
465
        self._unparsed = []
466
        for term in unparsed:
467
            self._unparsed += split_input(' \'%s\' ' % term)
468
        self._parser_modified = False