1 # Copyright 2012-2013 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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.
34 from kamaki.cli.config import Config
35 from kamaki.cli.errors import CLISyntaxError, raiseCLIError
36 from kamaki.cli.utils import split_input
38 from datetime import datetime as dtm
39 from time import mktime
41 from logging import getLogger
42 from argparse import ArgumentParser, ArgumentError
43 from argparse import RawDescriptionHelpFormatter
46 from progress.bar import ShadyBar as KamakiProgressBar
49 from progress.bar import Bar as KamakiProgressBar
52 # progress not installed - pls, pip install progress
55 log = getLogger(__name__)
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.
64 def __init__(self, arity, help=None, parsed_name=None, default=None):
65 self.arity = int(arity)
70 self.parsed_name = parsed_name
71 assert self.parsed_name, 'No parsed name for argument %s' % self
72 self.default = default
75 def parsed_name(self):
76 """the string which will be recognised by the parser as an instance
79 return getattr(self, '_parsed_name', None)
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)
87 self._parsed_name.append('%s' % newname)
91 """a user friendly help message"""
92 return getattr(self, '_help', None)
95 def help(self, newhelp):
96 self._help = '%s' % newhelp
100 """negative for repeating, 0 for flag, 1 or more for values"""
101 return getattr(self, '_arity', None)
104 def arity(self, newarity):
105 newarity = int(newarity)
106 self._arity = newarity
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
116 def default(self, newdefault):
117 self._default = newdefault
121 """the value of the argument"""
122 return getattr(self, '_value', self.default)
125 def value(self, newvalue):
126 self._value = newvalue
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\
137 default=self.default,
141 """Overide this method to give functionality to your args"""
142 raise NotImplementedError
145 class ConfigArgument(Argument):
146 """Manage a kamaki configuration (file)"""
152 """A Config object"""
153 super(self.__class__, self).value
154 return super(self.__class__, self).value
157 def value(self, 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)
164 self._value = Config()
166 def get(self, group, term):
167 """Get a configuration setting from the Config object"""
168 return self.value.get(group, term)
170 def get_groups(self):
173 return [term[:-slen] for term in self.value.keys('global') if (
174 term.endswith(suffix))]
176 def get_cli_specs(self):
179 return [(k[:-slen], v) for k, v in self.value.items('global') if (
182 def get_global(self, option):
183 return self.value.get_global(option)
185 def get_cloud(self, cloud, option):
186 return self.value.get_cloud(cloud, option)
188 _config_arg = ConfigArgument(
189 1, 'Path to configuration file', ('-c', '--config'))
192 class CmdLineConfigArgument(Argument):
193 """Set a run-time setting option (not persistent)"""
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
201 """A key=val option"""
202 return super(self.__class__, self).value
205 def value(self, options):
206 if options == self.default:
208 if not isinstance(options, list):
209 options = ['%s' % options]
210 for option in options:
211 keypath, sep, val = option.partition('=')
214 CLISyntaxError('Argument Syntax Error '),
216 '%s is missing a "="',
217 ' (usage: -o section.key=val)' % option])
218 section, sep, key = keypath.partition('.')
222 self._config_arg.value.override(
228 class FlagArgument(Argument):
230 :value: true if set, false otherwise
233 def __init__(self, help='', parsed_name=None, default=False):
234 super(FlagArgument, self).__init__(0, help, parsed_name, default)
237 class ValueArgument(Argument):
240 :value returns: given value or default
243 def __init__(self, help='', parsed_name=None, default=None):
244 super(ValueArgument, self).__init__(1, help, parsed_name, default)
247 class CommaSeparatedListArgument(ValueArgument):
250 :value returns: list of the comma separated values
255 return self._value or list()
258 def value(self, newvalue):
259 self._value = newvalue.split(',') if newvalue else list()
262 class IntArgument(ValueArgument):
266 """integer (type checking)"""
267 return getattr(self, '_value', self.default)
270 def value(self, newvalue):
271 if newvalue == self.default:
272 self._value = self.default
275 self._value = int(newvalue)
277 raiseCLIError(CLISyntaxError(
279 details=['Value %s not an int' % newvalue]))
282 class DateArgument(ValueArgument):
284 :value type: a string formated in an acceptable date format
286 :value returns: same date in first of 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"]
294 INPUT_FORMATS = DATE_FORMATS + ["%d-%m-%Y", "%H:%M:%S %d-%m-%Y"]
298 v = getattr(self, '_value', self.default)
299 return mktime(v.timetuple()) if v else None
303 v = getattr(self, '_value', self.default)
304 return v.strftime(self.DATE_FORMATS[0]) if v else None
308 return self.timestamp
311 def value(self, newvalue):
313 self._value = self.format_date(newvalue)
315 def format_date(self, datestr):
316 for format in self.INPUT_FORMATS:
318 t = dtm.strptime(datestr, format)
321 return t # .strftime(self.DATE_FORMATS[0])
324 'Date Argument Error',
325 details='%s not a valid date. correct formats:\n\t%s' % (
326 datestr, self.INPUT_FORMATS))
329 class VersionArgument(FlagArgument):
330 """A flag argument with that prints current version"""
335 return super(self.__class__, self).value
338 def value(self, newvalue):
339 self._value = newvalue
343 """Print current version"""
346 print('kamaki %s' % kamaki.__version__)
349 class KeyValueArgument(Argument):
350 """A Value Argument that can be repeated
352 :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
355 def __init__(self, help='', parsed_name=None, default=[]):
356 super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
362 :output: {'key1':'value1', 'key2':'value2', ...}
364 return super(KeyValueArgument, self).value
367 def value(self, keyvalue_pairs):
369 for pair in keyvalue_pairs:
370 key, sep, val = pair.partition('=')
373 CLISyntaxError('Argument syntax error '),
374 details='%s is missing a "=" (usage: key1=val1 )\n' % pair)
375 self._value[key.strip()] = val.strip()
378 class ProgressBarArgument(FlagArgument):
379 """Manage a progress bar"""
381 def __init__(self, help='', parsed_name='', default=True):
382 self.suffix = '%(percent)d%%'
383 super(ProgressBarArgument, self).__init__(help, parsed_name, default)
387 log.warning('WARNING: no progress bar functionality')
390 """Get a modifiable copy of this bar"""
391 newarg = ProgressBarArgument(
395 newarg._value = self._value
398 def get_generator(self, message, message_len=25):
399 """Get a generator to handle progress of the bar (gen.next())"""
403 self.bar = KamakiProgressBar()
407 self.bar.message = message.ljust(message_len)
408 self.bar.suffix = '%(percent)d%% - %(eta)ds'
412 for i in self.bar.iter(range(int(n))):
418 """Stop progress bar, return terminal cursor to user"""
421 mybar = getattr(self, 'bar', None)
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'))
441 # Initial command line interface arguments
444 class ArgumentParseManager(object):
445 """Manage (initialize and update) an ArgumentParser object"""
449 _parser_modified = False
453 def __init__(self, exe, arguments=None):
455 :param exe: (str) the basic command (e.g. 'kamaki')
457 :param arguments: (dict) if given, overrides the global _argument as
458 the parsers arguments specification
460 self.parser = ArgumentParser(
461 add_help=False, formatter_class=RawDescriptionHelpFormatter)
462 self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
464 self.arguments = arguments
467 self.arguments = _arguments
472 """The command syntax (useful for help messages, descriptions, etc)"""
473 return self.parser.prog
476 def syntax(self, new_syntax):
477 self.parser.prog = new_syntax
481 """(dict) arguments the parser should be aware of"""
482 return self._arguments
485 def arguments(self, new_arguments):
487 assert isinstance(new_arguments, dict)
488 self._arguments = new_arguments
493 """(Namespace) parser-matched terms"""
494 if self._parser_modified:
500 """(list) parser-unmatched terms"""
501 if self._parser_modified:
503 return self._unparsed
505 def update_parser(self, arguments=None):
506 """Load argument specifications to parser
508 :param arguments: if not given, update self.arguments instead
511 arguments = self._arguments
513 for name, arg in arguments.items():
515 arg.update_parser(self.parser, name)
516 self._parser_modified = True
517 except ArgumentError:
520 def update_arguments(self, new_arguments):
521 """Add to / update existing arguments
523 :param new_arguments: (dict)
526 assert isinstance(new_arguments, dict)
527 self._arguments.update(new_arguments)
530 def parse(self, new_args=None):
531 """Parse user input"""
533 pkargs = (new_args,) if new_args else ()
534 self._parsed, unparsed = self.parser.parse_known_args(*pkargs)
536 raiseCLIError(CLISyntaxError('Argument Syntax Error'))
537 for name, arg in self.arguments.items():
538 arg.value = getattr(self._parsed, name, arg.default)
540 for term in unparsed:
541 self._unparsed += split_input(' \'%s\' ' % term)
542 self._parser_modified = False