Statistics
| Branch: | Tag: | Revision:

root / kamaki / cli / argument.py @ 556e6916

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

    
67
        if help:
68
            self.help = help
69
        if parsed_name:
70
            self.parsed_name = parsed_name
71
        self.default = default
72

    
73
    @property
74
    def parsed_name(self):
75
        """the string which will be recognised by the parser as an instance
76
            of this argument
77
        """
78
        return getattr(self, '_parsed_name', None)
79

    
80
    @parsed_name.setter
81
    def parsed_name(self, newname):
82
        self._parsed_name = getattr(self, '_parsed_name', [])
83
        if isinstance(newname, list) or isinstance(newname, tuple):
84
            self._parsed_name += list(newname)
85
        else:
86
            self._parsed_name.append('%s' % newname)
87

    
88
    @property
89
    def help(self):
90
        """a user friendly help message"""
91
        return getattr(self, '_help', None)
92

    
93
    @help.setter
94
    def help(self, newhelp):
95
        self._help = '%s' % newhelp
96

    
97
    @property
98
    def arity(self):
99
        """negative for repeating, 0 for flag, 1 or more for values"""
100
        return getattr(self, '_arity', None)
101

    
102
    @arity.setter
103
    def arity(self, newarity):
104
        newarity = int(newarity)
105
        self._arity = newarity
106

    
107
    @property
108
    def default(self):
109
        """the value of this argument when not set"""
110
        if not hasattr(self, '_default'):
111
            self._default = False if self.arity == 0 else None
112
        return self._default
113

    
114
    @default.setter
115
    def default(self, newdefault):
116
        self._default = newdefault
117

    
118
    @property
119
    def value(self):
120
        """the value of the argument"""
121
        return getattr(self, '_value', self.default)
122

    
123
    @value.setter
124
    def value(self, newvalue):
125
        self._value = newvalue
126

    
127
    def update_parser(self, parser, name):
128
        """Update argument parser with self info"""
129
        action = 'append' if self.arity < 0\
130
            else 'store_true' if self.arity == 0\
131
            else 'store'
132
        parser.add_argument(
133
            *self.parsed_name,
134
            dest=name,
135
            action=action,
136
            default=self.default,
137
            help=self.help)
138

    
139
    def main(self):
140
        """Overide this method to give functionality to your args"""
141
        raise NotImplementedError
142

    
143

    
144
class ConfigArgument(Argument):
145
    """Manage a kamaki configuration (file)"""
146

    
147
    _config_file = None
148

    
149
    @property
150
    def value(self):
151
        """A Config object"""
152
        super(self.__class__, self).value
153
        return super(self.__class__, self).value
154

    
155
    @value.setter
156
    def value(self, config_file):
157
        if config_file:
158
            self._value = Config(config_file)
159
            self._config_file = config_file
160
        elif self._config_file:
161
            self._value = Config(self._config_file)
162
        else:
163
            self._value = Config()
164

    
165
    def get(self, group, term):
166
        """Get a configuration setting from the Config object"""
167
        return self.value.get(group, term)
168

    
169
    def get_groups(self):
170
        suffix = '_cli'
171
        slen = len(suffix)
172
        return [term[:-slen] for term in self.value.keys('global') if (
173
            term.endswith(suffix))]
174

    
175
    def get_cli_specs(self):
176
        suffix = '_cli'
177
        slen = len(suffix)
178
        return [(k[:-slen], v) for k, v in self.value.items('global') if (
179
            k.endswith(suffix))]
180

    
181
    def get_global(self, option):
182
        return self.value.get_global(option)
183

    
184
    def get_cloud(self, cloud, option):
185
        return self.value.get_cloud(cloud, option)
186

    
187
_config_arg = ConfigArgument(
188
    1, 'Path to configuration file', ('-c', '--config'))
189

    
190

    
191
class CmdLineConfigArgument(Argument):
192
    """Set a run-time setting option (not persistent)"""
193

    
194
    def __init__(self, config_arg, help='', parsed_name=None, default=None):
195
        super(self.__class__, self).__init__(1, help, parsed_name, default)
196
        self._config_arg = config_arg
197

    
198
    @property
199
    def value(self):
200
        """A key=val option"""
201
        return super(self.__class__, self).value
202

    
203
    @value.setter
204
    def value(self, options):
205
        if options == self.default:
206
            return
207
        if not isinstance(options, list):
208
            options = ['%s' % options]
209
        for option in options:
210
            keypath, sep, val = option.partition('=')
211
            if not sep:
212
                raiseCLIError(
213
                    CLISyntaxError('Argument Syntax Error '),
214
                    details=[
215
                        '%s is missing a "="',
216
                        ' (usage: -o section.key=val)' % option])
217
            section, sep, key = keypath.partition('.')
218
        if not sep:
219
            key = section
220
            section = 'global'
221
        self._config_arg.value.override(
222
            section.strip(),
223
            key.strip(),
224
            val.strip())
225

    
226

    
227
class FlagArgument(Argument):
228
    """
229
    :value: true if set, false otherwise
230
    """
231

    
232
    def __init__(self, help='', parsed_name=None, default=False):
233
        super(FlagArgument, self).__init__(0, help, parsed_name, default)
234

    
235

    
236
class ValueArgument(Argument):
237
    """
238
    :value type: string
239
    :value returns: given value or default
240
    """
241

    
242
    def __init__(self, help='', parsed_name=None, default=None):
243
        super(ValueArgument, self).__init__(1, help, parsed_name, default)
244

    
245

    
246
class IntArgument(ValueArgument):
247

    
248
    @property
249
    def value(self):
250
        """integer (type checking)"""
251
        return getattr(self, '_value', self.default)
252

    
253
    @value.setter
254
    def value(self, newvalue):
255
        if newvalue == self.default:
256
            self._value = self.default
257
            return
258
        try:
259
            self._value = int(newvalue)
260
        except ValueError:
261
            raiseCLIError(CLISyntaxError(
262
                'IntArgument Error',
263
                details=['Value %s not an int' % newvalue]))
264

    
265

    
266
class DateArgument(ValueArgument):
267
    """
268
    :value type: a string formated in an acceptable date format
269

270
    :value returns: same date in first of DATE_FORMATS
271
    """
272

    
273
    DATE_FORMATS = [
274
        "%a %b %d %H:%M:%S %Y",
275
        "%A, %d-%b-%y %H:%M:%S GMT",
276
        "%a, %d %b %Y %H:%M:%S GMT"]
277

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

    
280
    @property
281
    def timestamp(self):
282
        v = getattr(self, '_value', self.default)
283
        return mktime(v.timetuple()) if v else None
284

    
285
    @property
286
    def formated(self):
287
        v = getattr(self, '_value', self.default)
288
        return v.strftime(self.DATE_FORMATS[0]) if v else None
289

    
290
    @property
291
    def value(self):
292
        return self.timestamp
293

    
294
    @value.setter
295
    def value(self, newvalue):
296
        if newvalue:
297
            self._value = self.format_date(newvalue)
298

    
299
    def format_date(self, datestr):
300
        for format in self.INPUT_FORMATS:
301
            try:
302
                t = dtm.strptime(datestr, format)
303
            except ValueError:
304
                continue
305
            return t  # .strftime(self.DATE_FORMATS[0])
306
        raiseCLIError(
307
            None,
308
            'Date Argument Error',
309
            details='%s not a valid date. correct formats:\n\t%s' % (
310
                datestr, self.INPUT_FORMATS))
311

    
312

    
313
class VersionArgument(FlagArgument):
314
    """A flag argument with that prints current version"""
315

    
316
    @property
317
    def value(self):
318
        """bool"""
319
        return super(self.__class__, self).value
320

    
321
    @value.setter
322
    def value(self, newvalue):
323
        self._value = newvalue
324
        self.main()
325

    
326
    def main(self):
327
        """Print current version"""
328
        if self.value:
329
            import kamaki
330
            print('kamaki %s' % kamaki.__version__)
331

    
332

    
333
class KeyValueArgument(Argument):
334
    """A Value Argument that can be repeated
335

336
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
337
    """
338

    
339
    def __init__(self, help='', parsed_name=None, default=[]):
340
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
341

    
342
    @property
343
    def value(self):
344
        """
345
        :input: key=value
346
        :output: {'key1':'value1', 'key2':'value2', ...}
347
        """
348
        return super(KeyValueArgument, self).value
349

    
350
    @value.setter
351
    def value(self, keyvalue_pairs):
352
        self._value = {}
353
        for pair in keyvalue_pairs:
354
            key, sep, val = pair.partition('=')
355
            if not sep:
356
                raiseCLIError(
357
                    CLISyntaxError('Argument syntax error '),
358
                    details='%s is missing a "=" (usage: key1=val1 )\n' % pair)
359
            self._value[key.strip()] = val.strip()
360

    
361

    
362
class ProgressBarArgument(FlagArgument):
363
    """Manage a progress bar"""
364

    
365
    def __init__(self, help='', parsed_name='', default=True):
366
        self.suffix = '%(percent)d%%'
367
        super(ProgressBarArgument, self).__init__(help, parsed_name, default)
368
        try:
369
            KamakiProgressBar
370
        except NameError:
371
            log.warning('WARNING: no progress bar functionality')
372

    
373
    def clone(self):
374
        """Get a modifiable copy of this bar"""
375
        newarg = ProgressBarArgument(
376
            self.help,
377
            self.parsed_name,
378
            self.default)
379
        newarg._value = self._value
380
        return newarg
381

    
382
    def get_generator(self, message, message_len=25):
383
        """Get a generator to handle progress of the bar (gen.next())"""
384
        if self.value:
385
            return None
386
        try:
387
            self.bar = KamakiProgressBar()
388
        except NameError:
389
            self.value = None
390
            return self.value
391
        self.bar.message = message.ljust(message_len)
392
        self.bar.suffix = '%(percent)d%% - %(eta)ds'
393
        self.bar.start()
394

    
395
        def progress_gen(n):
396
            for i in self.bar.iter(range(int(n))):
397
                yield
398
            yield
399
        return progress_gen
400

    
401
    def finish(self):
402
        """Stop progress bar, return terminal cursor to user"""
403
        if self.value:
404
            return
405
        mybar = getattr(self, 'bar', None)
406
        if mybar:
407
            mybar.finish()
408

    
409

    
410
_arguments = dict(
411
    config=_config_arg,
412
    cloud=ValueArgument('Chose a cloud to connect to', ('--cloud')),
413
    help=Argument(0, 'Show help message', ('-h', '--help')),
414
    debug=FlagArgument('Include debug output', ('-d', '--debug')),
415
    include=FlagArgument(
416
        'Include raw connection data in the output', ('-i', '--include')),
417
    silent=FlagArgument('Do not output anything', ('-s', '--silent')),
418
    verbose=FlagArgument('More info at response', ('-v', '--verbose')),
419
    version=VersionArgument('Print current version', ('-V', '--version')),
420
    options=CmdLineConfigArgument(
421
        _config_arg, 'Override a config value', ('-o', '--options'))
422
)
423

    
424

    
425
#  Initial command line interface arguments
426

    
427

    
428
class ArgumentParseManager(object):
429
    """Manage (initialize and update) an ArgumentParser object"""
430

    
431
    parser = None
432
    _arguments = {}
433
    _parser_modified = False
434
    _parsed = None
435
    _unparsed = None
436

    
437
    def __init__(self, exe, arguments=None):
438
        """
439
        :param exe: (str) the basic command (e.g. 'kamaki')
440

441
        :param arguments: (dict) if given, overrides the global _argument as
442
            the parsers arguments specification
443
        """
444
        self.parser = ArgumentParser(
445
            add_help=False,
446
            formatter_class=RawDescriptionHelpFormatter)
447
        self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
448
        if arguments:
449
            self.arguments = arguments
450
        else:
451
            global _arguments
452
            self.arguments = _arguments
453
        self.parse()
454

    
455
    @property
456
    def syntax(self):
457
        """The command syntax (useful for help messages, descriptions, etc)"""
458
        return self.parser.prog
459

    
460
    @syntax.setter
461
    def syntax(self, new_syntax):
462
        self.parser.prog = new_syntax
463

    
464
    @property
465
    def arguments(self):
466
        """(dict) arguments the parser should be aware of"""
467
        return self._arguments
468

    
469
    @arguments.setter
470
    def arguments(self, new_arguments):
471
        if new_arguments:
472
            assert isinstance(new_arguments, dict)
473
        self._arguments = new_arguments
474
        self.update_parser()
475

    
476
    @property
477
    def parsed(self):
478
        """(Namespace) parser-matched terms"""
479
        if self._parser_modified:
480
            self.parse()
481
        return self._parsed
482

    
483
    @property
484
    def unparsed(self):
485
        """(list) parser-unmatched terms"""
486
        if self._parser_modified:
487
            self.parse()
488
        return self._unparsed
489

    
490
    def update_parser(self, arguments=None):
491
        """Load argument specifications to parser
492

493
        :param arguments: if not given, update self.arguments instead
494
        """
495
        if not arguments:
496
            arguments = self._arguments
497

    
498
        for name, arg in arguments.items():
499
            try:
500
                arg.update_parser(self.parser, name)
501
                self._parser_modified = True
502
            except ArgumentError:
503
                pass
504

    
505
    def update_arguments(self, new_arguments):
506
        """Add to / update existing arguments
507

508
        :param new_arguments: (dict)
509
        """
510
        if new_arguments:
511
            assert isinstance(new_arguments, dict)
512
            self._arguments.update(new_arguments)
513
            self.update_parser()
514

    
515
    def parse(self, new_args=None):
516
        """Do parse user input"""
517
        try:
518
            if new_args:
519
                self._parsed, unparsed = self.parser.parse_known_args(new_args)
520
            else:
521
                self._parsed, unparsed = self.parser.parse_known_args()
522
        except SystemExit:
523
            # deal with the fact that argparse error system is STUPID
524
            raiseCLIError(CLISyntaxError('Argument Syntax Error'))
525
        for name, arg in self.arguments.items():
526
            arg.value = getattr(self._parsed, name, arg.default)
527
        self._unparsed = []
528
        for term in unparsed:
529
            self._unparsed += split_input(' \'%s\' ' % term)
530
        self._parser_modified = False