b30e0121fcc17af46f9ede34ceda9bffb0e23c4a
[kamaki] / kamaki / cli / argument.py
1 #A. One-command CLI
2 #       1. Get a command string                 DONE
3 #       2. Parse out some Arguments     DONE
4 #               a. We need an Argument "library" for each command-level         DONE
5 #               b. Handle arg errors    
6 #       3. Retrieve and validate command_sequence
7 #               a. For faster responses, first command can be chosen from
8 #                       a prefixed list of names, loaded from the config file
9 #               b. Normally, each 1st level command has a file to read
10 #                       command specs from. Load command_specs in this file
11 #                       i. A dict with command specs is created
12 #                               e.g. {'store':{'list':{'all', None}, 'info'}, 'server':{'list', 'info'}}
13 #                               but in this case there will be only 'store', or 'server', etc.
14 #               c. Now, loop over the other parsed terms and check them against the commands
15 #                       i. That will produce a path of the form ['store', 'list' 'all']
16 #               d. Catch syntax errors
17 #       4. Instaciate object to exec
18 #               a. For path ['store', 'list', 'all'] instatiate store_list_all()
19 #       5. Parse out some more Arguments 
20 #               a. Each command path has an "Argument library" to check your args against
21 #       6. Call object.main() and catch ClientErrors
22 #               a. Now, there are some command-level syntax errors that we should catch
23 #                       as syntax errors? Maybe! Why not?
24
25 #Shell
26 #       1. Load ALL available command specs in advance
27 #       2. Iimport cmd (and run it ?)
28 #       3. There is probably a way to tell cmd of the command paths you support.
29 #       4. If cmd does not support it, for the sellected path call parse out stuff
30 #               as in One-command
31 #       5. Instatiate, parse_out and run object like in One-command
32 #       6. Run object.main() . Again, catch ClientErrors and, probably, syntax errors
33 import gevent.monkey
34 #Monkey-patch everything for gevent early on
35 gevent.monkey.patch_all()
36
37 from sys import exit
38
39 from .config import Config
40 from .errors import CLISyntaxError
41
42 class Argument(object):
43     """An argument that can be parsed from command line or otherwise"""
44
45     def __init__(self, arity, help=None, parsed_name=None, default=None):
46         self.arity = int(arity)
47
48         if help is not None:
49             self.help = help
50         if parsed_name is not None:
51             self.parsed_name = parsed_name
52         if default is not None:
53             self.default = default
54
55     @property 
56     def parsed_name(self):
57         return getattr(self, '_parsed_name', None)
58     @parsed_name.setter
59     def parsed_name(self, newname):
60         self._parsed_name = getattr(self, '_parsed_name', [])
61         if isinstance(newname, list) or isinstance(newname, tuple):
62             self._parsed_name += list(newname)
63         else:
64             self._parsed_name.append(unicode(newname))
65
66     @property 
67     def help(self):
68         return getattr(self, '_help', None)
69     @help.setter
70     def help(self, newhelp):
71         self._help = unicode(newhelp)
72
73     @property 
74     def arity(self):
75         return getattr(self, '_arity', None)
76     @arity.setter
77     def arity(self, newarity):
78         newarity = int(newarity)
79         assert newarity >= 0
80         self._arity = newarity
81
82     @property 
83     def default(self):
84         if not hasattr(self, '_default'):
85             self._default = False if self.arity == 0 else None
86         return self._default
87     @default.setter
88     def default(self, newdefault):
89         self._default = newdefault
90
91     @property 
92     def value(self):
93         return getattr(self, '_value', self.default)
94     @value.setter
95     def value(self, newvalue):
96         self._value = newvalue
97
98     def update_parser(self, parser, name):
99         """Update an argument parser with this argument info"""
100         action = 'store_true' if self.arity==0 else 'store'
101         parser.add_argument(*self.parsed_name, dest=name, action=action,
102             default=self.default, help=self.help)
103
104     def main(self):
105         """Overide this method to give functionality to ur args"""
106         raise NotImplementedError
107
108     @classmethod
109     def test(self):
110         h = Argument(arity=0, help='Display a help massage', parsed_name=('--help', '-h'))
111         b = Argument(arity=1, help='This is a bbb', parsed_name='--bbb')
112         c = Argument(arity=2, help='This is a ccc', parsed_name='--ccc')
113
114         from argparse import ArgumentParser
115         parser = ArgumentParser(add_help=False)
116         h.update_parser(parser, 'hee')
117         b.update_parser(parser, 'bee')
118         c.update_parser(parser, 'cee')
119
120         args, argv = parser.parse_known_args()
121         print('args: %s\nargv: %s'%(args, argv))
122
123 class VersionArgument(Argument):
124         @property 
125         def value(self):
126                 return super(self.__class__, self).value
127         @value.setter
128         def value(self, newvalue):
129                 self._value = newvalue
130                 self.main()
131
132         def main(self):
133                 if self.value:
134                         import kamaki
135                         print('kamaki %s'%kamaki.__version__)
136
137 class ConfigArgument(Argument):
138         @property 
139         def value(self):
140                 return super(self.__class__, self).value
141         @value.setter
142         def value(self, config_file):
143                 self._value = Config(config_file) if config_file is not None else Config()
144
145         def get_groups(self):
146                 return self.value.apis()
147
148
149 _config_arg = ConfigArgument(1, 'Path to configuration file', '--config')
150
151 class CmdLineConfigArgument(Argument):
152         def __init__(self, config_arg, help='', parsed_name=None, default=None):
153                 super(self.__class__, self).__init__(1, help, parsed_name, default)
154                 self._config_arg = config_arg
155
156         @property 
157         def value(self):
158                 return super(self.__class__, self).value
159         @value.setter
160         def value(self, options):
161                 if options == self.default:
162                         return
163                 options = [unicode(options)] if not isinstance(options, list) else options
164                 for option in options:
165                         keypath, sep, val = option.partition('=')
166                         if not sep:
167                                 raise CLISyntaxError(details='Missing = between key and value: -o section.key=val')
168                         section, sep, key = keypath.partition('.')
169                         if not sep:
170                                 raise CLISyntaxError(details='Missing . between section and key: -o section.key=val')
171                 self._config_arg.value.override(section.strip(), key.strip(), val.strip())
172
173 _arguments = dict(config = _config_arg,
174         debug = Argument(0, 'Include debug output', ('-d', '--debug')),
175         include = Argument(0, 'Include protocol headers in the output', ('-i', '--include')),
176         silent = Argument(0, 'Do not output anything', ('-s', '--silent')),
177         verbose = Argument(0, 'More info at response', ('-v', '--verbose')),
178         version = VersionArgument(0, 'Print current version', ('-V', '--version')),
179         options = CmdLineConfigArgument(_config_arg, 'Override a config value', ('-o', '--options'))
180 )
181
182 def parse_known_args(parser):
183         parsed, unparsed = parser.parse_known_args()
184         for name, arg in _arguments.items():
185                 arg.value = getattr(parsed, name, arg.value)
186         return parsed, unparsed
187
188