Revision 5286e2c3

/dev/null
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
        assert self.parsed_name, 'No parsed name for argument %s' % self
72
        self.default = default
73

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

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

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

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

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

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

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

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

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

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

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

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

  
144

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

  
148
    _config_file = None
149

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

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

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

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

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

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

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

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

  
191

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

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

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

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

  
227

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

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

  
236

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

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

  
246

  
247
class CommaSeparatedListArgument(ValueArgument):
248
    """
249
    :value type: string
250
    :value returns: list of the comma separated values
251
    """
252

  
253
    @property
254
    def value(self):
255
        return self._value or list()
256

  
257
    @value.setter
258
    def value(self, newvalue):
259
        self._value = newvalue.split(',') if newvalue else list()
260

  
261

  
262
class IntArgument(ValueArgument):
263

  
264
    @property
265
    def value(self):
266
        """integer (type checking)"""
267
        return getattr(self, '_value', self.default)
268

  
269
    @value.setter
270
    def value(self, newvalue):
271
        if newvalue == self.default:
272
            self._value = self.default
273
            return
274
        try:
275
            self._value = int(newvalue)
276
        except ValueError:
277
            raiseCLIError(CLISyntaxError(
278
                'IntArgument Error',
279
                details=['Value %s not an int' % newvalue]))
280

  
281

  
282
class DateArgument(ValueArgument):
283
    """
284
    :value type: a string formated in an acceptable date format
285

  
286
    :value returns: same date in first of DATE_FORMATS
287
    """
288

  
289
    DATE_FORMATS = [
290
        "%a %b %d %H:%M:%S %Y",
291
        "%A, %d-%b-%y %H:%M:%S GMT",
292
        "%a, %d %b %Y %H:%M:%S GMT"]
293

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

  
296
    @property
297
    def timestamp(self):
298
        v = getattr(self, '_value', self.default)
299
        return mktime(v.timetuple()) if v else None
300

  
301
    @property
302
    def formated(self):
303
        v = getattr(self, '_value', self.default)
304
        return v.strftime(self.DATE_FORMATS[0]) if v else None
305

  
306
    @property
307
    def value(self):
308
        return self.timestamp
309

  
310
    @value.setter
311
    def value(self, newvalue):
312
        if newvalue:
313
            self._value = self.format_date(newvalue)
314

  
315
    def format_date(self, datestr):
316
        for format in self.INPUT_FORMATS:
317
            try:
318
                t = dtm.strptime(datestr, format)
319
            except ValueError:
320
                continue
321
            return t  # .strftime(self.DATE_FORMATS[0])
322
        raiseCLIError(
323
            None,
324
            'Date Argument Error',
325
            details='%s not a valid date. correct formats:\n\t%s' % (
326
                datestr, self.INPUT_FORMATS))
327

  
328

  
329
class VersionArgument(FlagArgument):
330
    """A flag argument with that prints current version"""
331

  
332
    @property
333
    def value(self):
334
        """bool"""
335
        return super(self.__class__, self).value
336

  
337
    @value.setter
338
    def value(self, newvalue):
339
        self._value = newvalue
340
        self.main()
341

  
342
    def main(self):
343
        """Print current version"""
344
        if self.value:
345
            import kamaki
346
            print('kamaki %s' % kamaki.__version__)
347

  
348

  
349
class KeyValueArgument(Argument):
350
    """A Value Argument that can be repeated
351

  
352
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
353
    """
354

  
355
    def __init__(self, help='', parsed_name=None, default=[]):
356
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
357

  
358
    @property
359
    def value(self):
360
        """
361
        :input: key=value
362
        :output: {'key1':'value1', 'key2':'value2', ...}
363
        """
364
        return super(KeyValueArgument, self).value
365

  
366
    @value.setter
367
    def value(self, keyvalue_pairs):
368
        self._value = {}
369
        for pair in keyvalue_pairs:
370
            key, sep, val = pair.partition('=')
371
            if not sep:
372
                raiseCLIError(
373
                    CLISyntaxError('Argument syntax error '),
374
                    details='%s is missing a "=" (usage: key1=val1 )\n' % pair)
375
            self._value[key.strip()] = val.strip()
376

  
377

  
378
class ProgressBarArgument(FlagArgument):
379
    """Manage a progress bar"""
380

  
381
    def __init__(self, help='', parsed_name='', default=True):
382
        self.suffix = '%(percent)d%%'
383
        super(ProgressBarArgument, self).__init__(help, parsed_name, default)
384
        try:
385
            KamakiProgressBar
386
        except NameError:
387
            log.warning('WARNING: no progress bar functionality')
388

  
389
    def clone(self):
390
        """Get a modifiable copy of this bar"""
391
        newarg = ProgressBarArgument(
392
            self.help,
393
            self.parsed_name,
394
            self.default)
395
        newarg._value = self._value
396
        return newarg
397

  
398
    def get_generator(self, message, message_len=25):
399
        """Get a generator to handle progress of the bar (gen.next())"""
400
        if self.value:
401
            return None
402
        try:
403
            self.bar = KamakiProgressBar()
404
        except NameError:
405
            self.value = None
406
            return self.value
407
        self.bar.message = message.ljust(message_len)
408
        self.bar.suffix = '%(percent)d%% - %(eta)ds'
409
        self.bar.start()
410

  
411
        def progress_gen(n):
412
            for i in self.bar.iter(range(int(n))):
413
                yield
414
            yield
415
        return progress_gen
416

  
417
    def finish(self):
418
        """Stop progress bar, return terminal cursor to user"""
419
        if self.value:
420
            return
421
        mybar = getattr(self, 'bar', None)
422
        if mybar:
423
            mybar.finish()
424

  
425

  
426
_arguments = dict(
427
    config=_config_arg,
428
    cloud=ValueArgument('Chose a cloud to connect to', ('--cloud')),
429
    help=Argument(0, 'Show help message', ('-h', '--help')),
430
    debug=FlagArgument('Include debug output', ('-d', '--debug')),
431
    include=FlagArgument(
432
        'Include raw connection data in the output', ('-i', '--include')),
433
    silent=FlagArgument('Do not output anything', ('-s', '--silent')),
434
    verbose=FlagArgument('More info at response', ('-v', '--verbose')),
435
    version=VersionArgument('Print current version', ('-V', '--version')),
436
    options=CmdLineConfigArgument(
437
        _config_arg, 'Override a config value', ('-o', '--options'))
438
)
439

  
440

  
441
#  Initial command line interface arguments
442

  
443

  
444
class ArgumentParseManager(object):
445
    """Manage (initialize and update) an ArgumentParser object"""
446

  
447
    parser = None
448
    _arguments = {}
449
    _parser_modified = False
450
    _parsed = None
451
    _unparsed = None
452

  
453
    def __init__(self, exe, arguments=None):
454
        """
455
        :param exe: (str) the basic command (e.g. 'kamaki')
456

  
457
        :param arguments: (dict) if given, overrides the global _argument as
458
            the parsers arguments specification
459
        """
460
        self.parser = ArgumentParser(
461
            add_help=False, formatter_class=RawDescriptionHelpFormatter)
462
        self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
463
        if arguments:
464
            self.arguments = arguments
465
        else:
466
            global _arguments
467
            self.arguments = _arguments
468
        self.parse()
469

  
470
    @property
471
    def syntax(self):
472
        """The command syntax (useful for help messages, descriptions, etc)"""
473
        return self.parser.prog
474

  
475
    @syntax.setter
476
    def syntax(self, new_syntax):
477
        self.parser.prog = new_syntax
478

  
479
    @property
480
    def arguments(self):
481
        """(dict) arguments the parser should be aware of"""
482
        return self._arguments
483

  
484
    @arguments.setter
485
    def arguments(self, new_arguments):
486
        if new_arguments:
487
            assert isinstance(new_arguments, dict)
488
        self._arguments = new_arguments
489
        self.update_parser()
490

  
491
    @property
492
    def parsed(self):
493
        """(Namespace) parser-matched terms"""
494
        if self._parser_modified:
495
            self.parse()
496
        return self._parsed
497

  
498
    @property
499
    def unparsed(self):
500
        """(list) parser-unmatched terms"""
501
        if self._parser_modified:
502
            self.parse()
503
        return self._unparsed
504

  
505
    def update_parser(self, arguments=None):
506
        """Load argument specifications to parser
507

  
508
        :param arguments: if not given, update self.arguments instead
509
        """
510
        if not arguments:
511
            arguments = self._arguments
512

  
513
        for name, arg in arguments.items():
514
            try:
515
                arg.update_parser(self.parser, name)
516
                self._parser_modified = True
517
            except ArgumentError:
518
                pass
519

  
520
    def update_arguments(self, new_arguments):
521
        """Add to / update existing arguments
522

  
523
        :param new_arguments: (dict)
524
        """
525
        if new_arguments:
526
            assert isinstance(new_arguments, dict)
527
            self._arguments.update(new_arguments)
528
            self.update_parser()
529

  
530
    def parse(self, new_args=None):
531
        """Parse user input"""
532
        try:
533
            pkargs = (new_args,) if new_args else ()
534
            self._parsed, unparsed = self.parser.parse_known_args(*pkargs)
535
        except SystemExit:
536
            raiseCLIError(CLISyntaxError('Argument Syntax Error'))
537
        for name, arg in self.arguments.items():
538
            arg.value = getattr(self._parsed, name, arg.default)
539
        self._unparsed = []
540
        for term in unparsed:
541
            self._unparsed += split_input(' \'%s\' ' % term)
542
        self._parser_modified = False
b/kamaki/cli/argument/__init__.py
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
        self.parsed_name = parsed_name
68
        self.default = default or (None if self.arity else False)
69

  
70
    @property
71
    def parsed_name(self):
72
        """
73
        :returns: (str) of the form --smth or -s is recognised as a call to an
74
            argument instance
75
        """
76
        return getattr(self, '_parsed_name', [])
77

  
78
    @parsed_name.setter
79
    def parsed_name(self, newname):
80
        assert newname, 'No parsed name for argument %s' % self
81
        self._parsed_name = getattr(self, '_parsed_name', [])
82
        if isinstance(newname, list) or isinstance(newname, tuple):
83
            self._parsed_name += list(newname)
84
        else:
85
            self._parsed_name.append('%s' % newname)
86

  
87
    @property
88
    def value(self):
89
        """the value of the argument"""
90
        return getattr(self, '_value', self.default)
91

  
92
    @value.setter
93
    def value(self, newvalue):
94
        self._value = newvalue
95

  
96
    def update_parser(self, parser, name):
97
        """Update argument parser with self info"""
98
        action = 'append' if self.arity < 0\
99
            else 'store_true' if self.arity == 0\
100
            else 'store'
101
        parser.add_argument(
102
            *self.parsed_name,
103
            dest=name,
104
            action=action,
105
            default=self.default,
106
            help=self.help)
107

  
108
    def main(self):
109
        """Overide this method to give functionality to your args"""
110
        raise NotImplementedError
111

  
112

  
113
class ConfigArgument(Argument):
114
    """Manage a kamaki configuration (file)"""
115

  
116
    _config_file = None
117

  
118
    @property
119
    def value(self):
120
        """A Config object"""
121
        super(self.__class__, self).value
122
        return super(self.__class__, self).value
123

  
124
    @value.setter
125
    def value(self, config_file):
126
        if config_file:
127
            self._value = Config(config_file)
128
            self._config_file = config_file
129
        elif self._config_file:
130
            self._value = Config(self._config_file)
131
        else:
132
            self._value = Config()
133

  
134
    def get(self, group, term):
135
        """Get a configuration setting from the Config object"""
136
        return self.value.get(group, term)
137

  
138
    def get_groups(self):
139
        suffix = '_cli'
140
        slen = len(suffix)
141
        return [term[:-slen] for term in self.value.keys('global') if (
142
            term.endswith(suffix))]
143

  
144
    def get_cli_specs(self):
145
        suffix = '_cli'
146
        slen = len(suffix)
147
        return [(k[:-slen], v) for k, v in self.value.items('global') if (
148
            k.endswith(suffix))]
149

  
150
    def get_global(self, option):
151
        return self.value.get_global(option)
152

  
153
    def get_cloud(self, cloud, option):
154
        return self.value.get_cloud(cloud, option)
155

  
156
_config_arg = ConfigArgument(
157
    1, 'Path to configuration file', ('-c', '--config'))
158

  
159

  
160
class CmdLineConfigArgument(Argument):
161
    """Set a run-time setting option (not persistent)"""
162

  
163
    def __init__(self, config_arg, help='', parsed_name=None, default=None):
164
        super(self.__class__, self).__init__(1, help, parsed_name, default)
165
        self._config_arg = config_arg
166

  
167
    @property
168
    def value(self):
169
        """A key=val option"""
170
        return super(self.__class__, self).value
171

  
172
    @value.setter
173
    def value(self, options):
174
        if options == self.default:
175
            return
176
        if not isinstance(options, list):
177
            options = ['%s' % options]
178
        for option in options:
179
            keypath, sep, val = option.partition('=')
180
            if not sep:
181
                raiseCLIError(
182
                    CLISyntaxError('Argument Syntax Error '),
183
                    details=[
184
                        '%s is missing a "="',
185
                        ' (usage: -o section.key=val)' % option])
186
            section, sep, key = keypath.partition('.')
187
        if not sep:
188
            key = section
189
            section = 'global'
190
        self._config_arg.value.override(
191
            section.strip(),
192
            key.strip(),
193
            val.strip())
194

  
195

  
196
class FlagArgument(Argument):
197
    """
198
    :value: true if set, false otherwise
199
    """
200

  
201
    def __init__(self, help='', parsed_name=None, default=False):
202
        super(FlagArgument, self).__init__(0, help, parsed_name, default)
203

  
204

  
205
class ValueArgument(Argument):
206
    """
207
    :value type: string
208
    :value returns: given value or default
209
    """
210

  
211
    def __init__(self, help='', parsed_name=None, default=None):
212
        super(ValueArgument, self).__init__(1, help, parsed_name, default)
213

  
214

  
215
class CommaSeparatedListArgument(ValueArgument):
216
    """
217
    :value type: string
218
    :value returns: list of the comma separated values
219
    """
220

  
221
    @property
222
    def value(self):
223
        return self._value or list()
224

  
225
    @value.setter
226
    def value(self, newvalue):
227
        self._value = newvalue.split(',') if newvalue else list()
228

  
229

  
230
class IntArgument(ValueArgument):
231

  
232
    @property
233
    def value(self):
234
        """integer (type checking)"""
235
        return getattr(self, '_value', self.default)
236

  
237
    @value.setter
238
    def value(self, newvalue):
239
        if newvalue == self.default:
240
            self._value = self.default
241
            return
242
        try:
243
            self._value = int(newvalue)
244
        except ValueError:
245
            raiseCLIError(CLISyntaxError(
246
                'IntArgument Error',
247
                details=['Value %s not an int' % newvalue]))
248

  
249

  
250
class DateArgument(ValueArgument):
251
    """
252
    :value type: a string formated in an acceptable date format
253

  
254
    :value returns: same date in first of DATE_FORMATS
255
    """
256

  
257
    DATE_FORMATS = [
258
        "%a %b %d %H:%M:%S %Y",
259
        "%A, %d-%b-%y %H:%M:%S GMT",
260
        "%a, %d %b %Y %H:%M:%S GMT"]
261

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

  
264
    @property
265
    def timestamp(self):
266
        v = getattr(self, '_value', self.default)
267
        return mktime(v.timetuple()) if v else None
268

  
269
    @property
270
    def formated(self):
271
        v = getattr(self, '_value', self.default)
272
        return v.strftime(self.DATE_FORMATS[0]) if v else None
273

  
274
    @property
275
    def value(self):
276
        return self.timestamp
277

  
278
    @value.setter
279
    def value(self, newvalue):
280
        if newvalue:
281
            self._value = self.format_date(newvalue)
282

  
283
    def format_date(self, datestr):
284
        for format in self.INPUT_FORMATS:
285
            try:
286
                t = dtm.strptime(datestr, format)
287
            except ValueError:
288
                continue
289
            return t  # .strftime(self.DATE_FORMATS[0])
290
        raiseCLIError(
291
            None,
292
            'Date Argument Error',
293
            details='%s not a valid date. correct formats:\n\t%s' % (
294
                datestr, self.INPUT_FORMATS))
295

  
296

  
297
class VersionArgument(FlagArgument):
298
    """A flag argument with that prints current version"""
299

  
300
    @property
301
    def value(self):
302
        """bool"""
303
        return super(self.__class__, self).value
304

  
305
    @value.setter
306
    def value(self, newvalue):
307
        self._value = newvalue
308
        self.main()
309

  
310
    def main(self):
311
        """Print current version"""
312
        if self.value:
313
            import kamaki
314
            print('kamaki %s' % kamaki.__version__)
315

  
316

  
317
class KeyValueArgument(Argument):
318
    """A Value Argument that can be repeated
319

  
320
    :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
321
    """
322

  
323
    def __init__(self, help='', parsed_name=None, default=[]):
324
        super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
325

  
326
    @property
327
    def value(self):
328
        """
329
        :input: key=value
330
        :output: {'key1':'value1', 'key2':'value2', ...}
331
        """
332
        return super(KeyValueArgument, self).value
333

  
334
    @value.setter
335
    def value(self, keyvalue_pairs):
336
        self._value = {}
337
        for pair in keyvalue_pairs:
338
            key, sep, val = pair.partition('=')
339
            if not sep:
340
                raiseCLIError(
341
                    CLISyntaxError('Argument syntax error '),
342
                    details='%s is missing a "=" (usage: key1=val1 )\n' % pair)
343
            self._value[key.strip()] = val.strip()
344

  
345

  
346
class ProgressBarArgument(FlagArgument):
347
    """Manage a progress bar"""
348

  
349
    def __init__(self, help='', parsed_name='', default=True):
350
        self.suffix = '%(percent)d%%'
351
        super(ProgressBarArgument, self).__init__(help, parsed_name, default)
352
        try:
353
            KamakiProgressBar
354
        except NameError:
355
            log.warning('WARNING: no progress bar functionality')
356

  
357
    def clone(self):
358
        """Get a modifiable copy of this bar"""
359
        newarg = ProgressBarArgument(
360
            self.help,
361
            self.parsed_name,
362
            self.default)
363
        newarg._value = self._value
364
        return newarg
365

  
366
    def get_generator(self, message, message_len=25):
367
        """Get a generator to handle progress of the bar (gen.next())"""
368
        if self.value:
369
            return None
370
        try:
371
            self.bar = KamakiProgressBar()
372
        except NameError:
373
            self.value = None
374
            return self.value
375
        self.bar.message = message.ljust(message_len)
376
        self.bar.suffix = '%(percent)d%% - %(eta)ds'
377
        self.bar.start()
378

  
379
        def progress_gen(n):
380
            for i in self.bar.iter(range(int(n))):
381
                yield
382
            yield
383
        return progress_gen
384

  
385
    def finish(self):
386
        """Stop progress bar, return terminal cursor to user"""
387
        if self.value:
388
            return
389
        mybar = getattr(self, 'bar', None)
390
        if mybar:
391
            mybar.finish()
392

  
393

  
394
_arguments = dict(
395
    config=_config_arg,
396
    cloud=ValueArgument('Chose a cloud to connect to', ('--cloud')),
397
    help=Argument(0, 'Show help message', ('-h', '--help')),
398
    debug=FlagArgument('Include debug output', ('-d', '--debug')),
399
    include=FlagArgument(
400
        'Include raw connection data in the output', ('-i', '--include')),
401
    silent=FlagArgument('Do not output anything', ('-s', '--silent')),
402
    verbose=FlagArgument('More info at response', ('-v', '--verbose')),
403
    version=VersionArgument('Print current version', ('-V', '--version')),
404
    options=CmdLineConfigArgument(
405
        _config_arg, 'Override a config value', ('-o', '--options'))
406
)
407

  
408

  
409
#  Initial command line interface arguments
410

  
411

  
412
class ArgumentParseManager(object):
413
    """Manage (initialize and update) an ArgumentParser object"""
414

  
415
    parser = None
416
    _arguments = {}
417
    _parser_modified = False
418
    _parsed = None
419
    _unparsed = None
420

  
421
    def __init__(self, exe, arguments=None):
422
        """
423
        :param exe: (str) the basic command (e.g. 'kamaki')
424

  
425
        :param arguments: (dict) if given, overrides the global _argument as
426
            the parsers arguments specification
427
        """
428
        self.parser = ArgumentParser(
429
            add_help=False, formatter_class=RawDescriptionHelpFormatter)
430
        self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
431
        if arguments:
432
            self.arguments = arguments
433
        else:
434
            global _arguments
435
            self.arguments = _arguments
436
        self.parse()
437

  
438
    @property
439
    def syntax(self):
440
        """The command syntax (useful for help messages, descriptions, etc)"""
441
        return self.parser.prog
442

  
443
    @syntax.setter
444
    def syntax(self, new_syntax):
445
        self.parser.prog = new_syntax
446

  
447
    @property
448
    def arguments(self):
449
        """(dict) arguments the parser should be aware of"""
450
        return self._arguments
451

  
452
    @arguments.setter
453
    def arguments(self, new_arguments):
454
        if new_arguments:
455
            assert isinstance(new_arguments, dict)
456
        self._arguments = new_arguments
457
        self.update_parser()
458

  
459
    @property
460
    def parsed(self):
461
        """(Namespace) parser-matched terms"""
462
        if self._parser_modified:
463
            self.parse()
464
        return self._parsed
465

  
466
    @property
467
    def unparsed(self):
468
        """(list) parser-unmatched terms"""
469
        if self._parser_modified:
470
            self.parse()
471
        return self._unparsed
472

  
473
    def update_parser(self, arguments=None):
474
        """Load argument specifications to parser
475

  
476
        :param arguments: if not given, update self.arguments instead
477
        """
478
        if not arguments:
479
            arguments = self._arguments
480

  
481
        for name, arg in arguments.items():
482
            try:
483
                arg.update_parser(self.parser, name)
484
                self._parser_modified = True
485
            except ArgumentError:
486
                pass
487

  
488
    def update_arguments(self, new_arguments):
489
        """Add to / update existing arguments
490

  
491
        :param new_arguments: (dict)
492
        """
493
        if new_arguments:
494
            assert isinstance(new_arguments, dict)
495
            self._arguments.update(new_arguments)
496
            self.update_parser()
497

  
498
    def parse(self, new_args=None):
499
        """Parse user input"""
500
        try:
501
            pkargs = (new_args,) if new_args else ()
502
            self._parsed, unparsed = self.parser.parse_known_args(*pkargs)
503
        except SystemExit:
504
            raiseCLIError(CLISyntaxError('Argument Syntax Error'))
505
        for name, arg in self.arguments.items():
506
            arg.value = getattr(self._parsed, name, arg.default)
507
        self._unparsed = []
508
        for term in unparsed:
509
            self._unparsed += split_input(' \'%s\' ' % term)
510
        self._parser_modified = False
b/kamaki/cli/argument/test.py
1
# Copyright 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 mock import patch, call
35
from unittest import TestCase
36
#from itertools import product
37

  
38
from kamaki.cli import argument
39

  
40

  
41
class Argument(TestCase):
42

  
43
    def test___init__(self):
44
        self.assertRaises(ValueError, argument.Argument, 'non-integer')
45
        self.assertRaises(AssertionError, argument.Argument, 1)
46
        for arity, help, parsed_name, default in (
47
                (0, 'help 0', '--zero', None),
48
                (1, 'help 1', ['--one', '-o'], 'lala'),
49
                (-1, 'help -1', ['--help', '--or', '--more'], 0),
50
                (0, 'help 0 again', ['--again', ], True)):
51
            a = argument.Argument(arity, help, parsed_name, default)
52
            if arity:
53
                self.assertEqual(arity, a.arity)
54
            self.assertEqual(help, a.help)
55

  
56
            exp_name = parsed_name if (
57
                isinstance(parsed_name, list)) else [parsed_name, ]
58
            self.assertEqual(exp_name, a.parsed_name)
59

  
60
            exp_default = default or (None if arity else False)
61
            self.assertEqual(exp_default, a.default)
62

  
63

  
64
if __name__ == '__main__':
65
    from sys import argv
66
    from kamaki.cli.test import runTestCase
67
    runTestCase(Argument, 'Argument', argv[1:])
b/kamaki/cli/test.py
31 31
# interpreted as representing official policies, either expressed
32 32
# or implied, of GRNET S.A.
33 33

  
34
from mock import patch, call
35 34
from unittest import makeSuite, TestSuite, TextTestRunner, TestCase
36
from time import sleep
37 35
from inspect import getmembers, isclass
38
from itertools import product
39
from random import randint
40 36

  
41 37
from kamaki.cli.command_tree.test import Command, CommandTree
38
from kamaki.cli.argument.test import Argument
42 39

  
43 40

  
44 41
#  TestCase auxiliary methods

Also available in: Unified diff