1 # Copyright 2012 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
36 from kamaki.cli.utils import split_input
38 from argparse import ArgumentParser, ArgumentError
41 from progress.bar import FillingCirclesBar as KamakiProgressBar
44 # progress not installed - pls, pip install progress
48 class Argument(object):
49 """An argument that can be parsed from command line or otherwise.
50 This is the general Argument class. It is suggested to extent this
51 class into more specific argument types.
54 def __init__(self, arity, help=None, parsed_name=None, default=None):
55 self.arity = int(arity)
59 if parsed_name is not None:
60 self.parsed_name = parsed_name
61 if default is not None:
62 self.default = default
65 def parsed_name(self):
66 """the string which will be recognised by the parser as an instance
69 return getattr(self, '_parsed_name', None)
72 def parsed_name(self, newname):
73 self._parsed_name = getattr(self, '_parsed_name', [])
74 if isinstance(newname, list) or isinstance(newname, tuple):
75 self._parsed_name += list(newname)
77 self._parsed_name.append(unicode(newname))
81 """a user friendly help message"""
82 return getattr(self, '_help', None)
85 def help(self, newhelp):
86 self._help = unicode(newhelp)
90 """negative for repeating, 0 for flag, 1 or more for values"""
91 return getattr(self, '_arity', None)
94 def arity(self, newarity):
95 newarity = int(newarity)
96 self._arity = newarity
100 """the value of this argument when not set"""
101 if not hasattr(self, '_default'):
102 self._default = False if self.arity == 0 else None
106 def default(self, newdefault):
107 self._default = newdefault
111 """the value of the argument"""
112 return getattr(self, '_value', self.default)
115 def value(self, newvalue):
116 self._value = newvalue
118 def update_parser(self, parser, name):
119 """Update argument parser with self info"""
120 action = 'append' if self.arity < 0\
121 else 'store_true' if self.arity == 0\
123 parser.add_argument(*self.parsed_name, dest=name, action=action,
124 default=self.default, help=self.help)
127 """Overide this method to give functionality to your args"""
128 raise NotImplementedError
131 class ConfigArgument(Argument):
132 """Manage a kamaki configuration (file)"""
138 """A Config object"""
139 super(self.__class__, self).value
140 return super(self.__class__, self).value
143 def value(self, config_file):
145 self._value = Config(config_file)
146 self._config_file = config_file
147 elif self._config_file:
148 self._value = Config(self._config_file)
150 self._value = Config()
152 def get(self, group, term):
153 """Get a configuration setting from the Config object"""
154 return self.value.get(group, term)
156 def get_groups(self):
157 return self.value.apis()
159 _config_arg = ConfigArgument(1, 'Path to configuration file', '--config')
162 class CmdLineConfigArgument(Argument):
163 """Set a run-time setting option (not persistent)"""
165 def __init__(self, config_arg, help='', parsed_name=None, default=None):
166 super(self.__class__, self).__init__(1, help, parsed_name, default)
167 self._config_arg = config_arg
171 """A key=val option"""
172 return super(self.__class__, self).value
175 def value(self, options):
176 if options == self.default:
178 if not isinstance(options, list):
179 options = [unicode(options)]
180 for option in options:
181 keypath, sep, val = option.partition('=')
183 raise CLISyntaxError('Argument Syntax Error ',
184 details='%s is missing a "=" (usage: -o section.key=val)'\
186 section, sep, key = keypath.partition('.')
190 self._config_arg.value.override(
196 class FlagArgument(Argument):
198 :value: true if set, false otherwise
201 def __init__(self, help='', parsed_name=None, default=False):
202 super(FlagArgument, self).__init__(0, help, parsed_name, default)
205 class ValueArgument(Argument):
208 :value returns: given value or default
211 def __init__(self, help='', parsed_name=None, default=None):
212 super(ValueArgument, self).__init__(1, help, parsed_name, default)
215 class IntArgument(ValueArgument):
219 """integer (type checking)"""
220 return getattr(self, '_value', self.default)
223 def value(self, newvalue):
224 if newvalue == self.default:
225 self._value = self.default
228 self._value = int(newvalue)
230 raise CLISyntaxError('IntArgument Error',
231 details='Value %s not an int' % newvalue)
234 class VersionArgument(FlagArgument):
235 """A flag argument with that prints current version"""
240 return super(self.__class__, self).value
243 def value(self, newvalue):
244 self._value = newvalue
248 """Print current version"""
251 print('kamaki %s' % kamaki.__version__)
254 class KeyValueArgument(Argument):
255 """A Value Argument that can be repeated
257 :syntax: --<arg> key1=value1 --<arg> key2=value2 ...
260 def __init__(self, help='', parsed_name=None, default=[]):
261 super(KeyValueArgument, self).__init__(-1, help, parsed_name, default)
267 :output: {'key1':'value1', 'key2':'value2', ...}
269 return super(KeyValueArgument, self).value
272 def value(self, keyvalue_pairs):
274 for pair in keyvalue_pairs:
275 key, sep, val = pair.partition('=')
277 raise CLISyntaxError('Argument syntax error ',
278 details='%s is missing a "=" (usage: key1=val1 )\n' % pair)
279 self._value[key.strip()] = val.strip()
282 class ProgressBarArgument(FlagArgument):
283 """Manage a progress bar"""
285 def __init__(self, help='', parsed_name='', default=True):
286 self.suffix = '%(percent)d%%'
287 super(ProgressBarArgument, self).__init__(help, parsed_name, default)
291 print('Warning: no progress bar functionality')
294 """Get a modifiable copy of this bar"""
295 newarg = ProgressBarArgument(
299 newarg._value = self._value
302 def get_generator(self, message, message_len=25):
303 """Get a generator to handle progress of the bar (gen.next())"""
307 self.bar = KamakiProgressBar()
311 self.bar.message = message.ljust(message_len)
312 self.bar.suffix = '%(percent)d%% - %(eta)ds'
316 for i in self.bar.iter(range(int(n))):
322 """Stop progress bar, return terminal cursor to user"""
325 mybar = getattr(self, 'bar', None)
330 _arguments = dict(config=_config_arg,
331 help=Argument(0, 'Show help message', ('-h', '--help')),
332 debug=FlagArgument('Include debug output', ('-d', '--debug')),
333 include=FlagArgument('Include protocol headers in the output',
334 ('-i', '--include')),
335 silent=FlagArgument('Do not output anything', ('-s', '--silent')),
336 verbose=FlagArgument('More info at response', ('-v', '--verbose')),
337 version=VersionArgument('Print current version', ('-V', '--version')),
338 options=CmdLineConfigArgument(_config_arg,
339 'Override a config value',
342 """Initial command line interface arguments"""
349 manage top-level user arguments input
350 find user-requested command
351 add command-specific arguments to dict
356 class ArgumentParseManager():
357 """Manage (initialize and update) an ArgumentParser object"""
359 parser = ArgumentParser(add_help=False)
362 def __init__(self, exe, arguments=None):
364 :param exe: (str) the basic command (e.g. 'kamaki')
366 :param arguments: (dict) if given, overrides the global _argument as
367 the parsers arguments specification
370 self.arguments = arguments
373 self.arguments = _arguments
375 self.syntax = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
380 return self.parser.prog
383 def syntax(self, new_syntax):
384 self.parser.prog = new_syntax
386 def update_parser(self, arguments=None):
387 """Load argument specifications to parser
389 :param arguments: if not given, update self.arguments instead
392 arguments = self.arguments
394 for name, arg in arguments.items():
396 arg.update_parser(self.parser, name)
397 except ArgumentError:
402 def init_parser(exe, arguments):
403 ""Create and initialize an ArgumentParser object""
404 parser = ArgumentParser(add_help=False)
405 parser.prog = '%s <cmd_group> [<cmd_subbroup> ...] <cmd>' % exe
406 update_arguments(parser, arguments)
411 def parse_known_args(parser, arguments=None):
412 """Fill in arguments from user input"""
413 parsed, unparsed = parser.parse_known_args()
414 for name, arg in arguments.items():
415 arg.value = getattr(parsed, name, arg.default)
417 for term in unparsed:
418 newparsed += split_input(' \'%s\' ' % term)
419 return parsed, newparsed
422 def update_arguments(parser, arguments):
423 """Update arguments dict from user input
426 for name, argument in arguments.items():
428 argument.update_parser(parser, name)
429 except ArgumentError: