root / docs / developers / adding-commands.rst @ dc99e627
History | View | Annotate | Download (13.2 kB)
1 |
Adding Commands |
---|---|
2 |
=============== |
3 |
|
4 |
Kamaki commands are implemented as python classes, which wear a decorator |
5 |
called *command*. The decorator lives in *kamaki.cli* and its purpose is to |
6 |
update the *CommandTree* structure. The *CommandTree* class ( |
7 |
*kamaki.cli.commant_tree*) manages command namespaces for kamaki. |
8 |
|
9 |
For demonstration purposes, the following set of kamaki commands will be |
10 |
implemented in this document:: |
11 |
|
12 |
mygrp1 list all //show a list |
13 |
mygrp1 list details [--match=<>] //show list of details |
14 |
mygrp2 list all [regular expression] [-l] //list all subjects |
15 |
mygrp2 info <id> [--filter] //information on a subject |
16 |
|
17 |
.. note:: By convention, the names of the groups describe subjects e.g., |
18 |
"server", "network", "container", etc. |
19 |
|
20 |
Here we get two command groups to implement i.e., *mygrp1* and *mygrp2*, |
21 |
containing two commands each (*list_all*, *list_details* and *list_all*, *info* |
22 |
respectively). The underscore is used to separate command namespaces and should |
23 |
be considered as a special character in this context. |
24 |
|
25 |
The first command (*mygrp1_list_all*) has the simplest possible syntax: no |
26 |
parameters, no runtime arguments. The second one defines one optional runtime |
27 |
argument with a value. The third features an optional parameter and an optional |
28 |
runtime flag argument. The last one is an example of a command with an |
29 |
obligatory and an optional parameter. |
30 |
|
31 |
Some examples: |
32 |
|
33 |
.. code-block:: console |
34 |
|
35 |
$kamaki mygrp1 |
36 |
mygrp1 description |
37 |
|
38 |
Options |
39 |
- - - - |
40 |
list |
41 |
$ kamaki mygrp1 list |
42 |
|
43 |
Options |
44 |
- - - - |
45 |
all show a list |
46 |
details show a list of details |
47 |
$ kamaki mygrp1 list all |
48 |
... (a mygrp1_list_all instance runs) ... |
49 |
$ kamaki mygrp2 list all 'Z[.]' -l |
50 |
... (a mygrp2_list_all instance runs) ... |
51 |
$ |
52 |
|
53 |
The CommandTree structure |
54 |
------------------------- |
55 |
|
56 |
CommandTree manages commands and their namespaces. Each command is stored in |
57 |
a tree, where each node is a name. A leaf is the rightmost term of a namespace |
58 |
and contains a pointer to the executable command class. |
59 |
|
60 |
Here is an example from the actual kamaki command structure, featuring the |
61 |
commands *file upload*, *file list* and *file info* :: |
62 |
|
63 |
- file |
64 |
''''''''|- info |
65 |
|- list |
66 |
|- upload |
67 |
|
68 |
Now, let's load the showcase example on CommandTrees:: |
69 |
|
70 |
- mygrp1 |
71 |
''''''''|- list |
72 |
'''''''|- all |
73 |
|- details |
74 |
|
75 |
- mygrp2 |
76 |
''''''''|- list |
77 |
'''''''|- all |
78 |
|- info |
79 |
|
80 |
Each command group should be stored on a different CommandTree. |
81 |
|
82 |
For that reason, command specification modules should contain a list of |
83 |
CommandTree objects, named *_commands*. This mechanism allows any interface |
84 |
application to load the list of commands from the *_commands* array. |
85 |
|
86 |
.. code-block:: python |
87 |
|
88 |
_mygrp1_commands = CommandTree('mygrp', 'mygrp1 description') |
89 |
_mygrp2_commands = CommandTree('mygrp', 'mygrp2 description') |
90 |
|
91 |
_commands = [_mygrp1_commands, _mygrp2_commands] |
92 |
|
93 |
.. note:: The name and the description, will later appear in automatically |
94 |
created help messages |
95 |
|
96 |
The command decorator |
97 |
--------------------- |
98 |
|
99 |
All commands are specified by subclasses of *kamaki.cli.commands._command_init* |
100 |
These classes are called "command specifications". |
101 |
|
102 |
The *command* decorator mines all the information needed to build namespaces |
103 |
from a command specification:: |
104 |
|
105 |
class code ---> command() --> updated CommandTree structure |
106 |
|
107 |
Kamaki interfaces make use of the CommandTree structure. Optimizations are |
108 |
possible by using special parameters on the command decorator method. |
109 |
|
110 |
.. code-block:: python |
111 |
|
112 |
def command(cmd_tree, prefix='', descedants_depth=None): |
113 |
"""Load a class as a command |
114 |
|
115 |
:param cmd_tree: is the CommandTree to be updated with a new command |
116 |
|
117 |
:param prefix: of the commands allowed to be inserted ('' for all) |
118 |
|
119 |
:param descedants_depth: is the depth of the tree descendants of the |
120 |
prefix command. |
121 |
""" |
122 |
|
123 |
Creating a new command specification set |
124 |
---------------------------------------- |
125 |
|
126 |
A command specification developer should create a new module (python file) with |
127 |
one command specification class per command. Each class should be decorated |
128 |
with *command*. |
129 |
|
130 |
.. code-block:: python |
131 |
|
132 |
... |
133 |
_commands = [_mygrp1_commands, _mygrp2_commands] |
134 |
|
135 |
@command(_mygrp1_commands) |
136 |
class mygrp1_list_all(): |
137 |
... |
138 |
|
139 |
... |
140 |
|
141 |
A list of CommandTree structures must exist in the module scope, with the name |
142 |
*_commands*. Different CommandTree objects correspond to different command |
143 |
groups. |
144 |
|
145 |
Set command description |
146 |
----------------------- |
147 |
|
148 |
The first line of the class commend is used as the command short description. |
149 |
The rest is used as the detailed description. |
150 |
|
151 |
.. code-block:: python |
152 |
|
153 |
... |
154 |
@command(_mygrp2_commands) |
155 |
class mygrp2_info(): |
156 |
"""get information for subject with id |
157 |
Anything from this point and bellow constitutes the long description |
158 |
Please, mind the indentation, pep8 is not forgiving. |
159 |
""" |
160 |
... |
161 |
|
162 |
Description placeholders |
163 |
------------------------ |
164 |
|
165 |
There is possible to create an empty command, that can act as a description |
166 |
placeholder. For example, the *mygrp1_list* namespace does not correspond to an |
167 |
executable command, but it can have a helpful description. In that case, create |
168 |
a command specification class with a command and no code: |
169 |
|
170 |
.. code-block:: python |
171 |
|
172 |
@command(_mygrp1_commands) |
173 |
class mygrp1_list(): |
174 |
"""List mygrp1 objects. |
175 |
There are two versions: short and detailed |
176 |
""" |
177 |
|
178 |
.. warning:: A command specification class with no description is invalid and |
179 |
will cause an error. |
180 |
|
181 |
Declare run-time argument |
182 |
------------------------- |
183 |
|
184 |
The argument mechanism is based on the standard argparse module. |
185 |
|
186 |
Some basic argument types are defined at the |
187 |
`argument module <code.html#module-kamaki.cli.argument>`_, but it is not |
188 |
a bad idea to extent these classes in order to achieve specialized type |
189 |
checking and syntax control with respect to the semantics of each command. |
190 |
Still, in most cases, the argument types of the argument package are enough for |
191 |
most cases. |
192 |
|
193 |
To declare a run-time argument on a specific command, the specification class |
194 |
should contain a dict called *arguments* , where Argument objects are stored. |
195 |
Each argument object is a run-time argument. Syntax checking happens at the |
196 |
command specification level, while the type checking is implemented in the |
197 |
Argument subclasses. |
198 |
|
199 |
.. code-block:: python |
200 |
|
201 |
from kamaki.cli.argument import ValueArgument |
202 |
... |
203 |
|
204 |
@command(_mygrp1_commands) |
205 |
class mygrp1_list_details(): |
206 |
"""list of details""" |
207 |
|
208 |
def __init__(self, global_args={}): |
209 |
global_args['match'] = ValueArgument( |
210 |
'Filter results to match string', |
211 |
('-m', '--match')) |
212 |
self.arguments = global_args |
213 |
|
214 |
or more usually and elegantly: |
215 |
|
216 |
.. code-block:: python |
217 |
|
218 |
from kamaki.cli.argument import ValueArgument |
219 |
|
220 |
@command(_mygrp1_commands) |
221 |
class mygrp1_list_details(): |
222 |
"""List of details""" |
223 |
|
224 |
arguments = dict( |
225 |
match=ValueArgument( |
226 |
'Filter output to match string', ('-m', --match')) |
227 |
) |
228 |
|
229 |
Accessing run-time arguments |
230 |
---------------------------- |
231 |
|
232 |
To access run-time arguments, command classes extend the *_command_init* |
233 |
interface, which implements *__item__* accessors to handle run-time argument |
234 |
values. In other words, one may get the runtime value of an argument by calling |
235 |
*self[<argument>]*. |
236 |
|
237 |
.. code-block:: python |
238 |
|
239 |
from kamaki.cli.argument import ValueArgument |
240 |
from kamaki.cli.commands import _command_init |
241 |
|
242 |
@command(_mygrp1_commands) |
243 |
class mygrp1_list_details(_command_init): |
244 |
"""List of details""" |
245 |
|
246 |
arguments = dict( |
247 |
match=ValueArgument( |
248 |
'Filter output to match string', ('-m', --match')) |
249 |
) |
250 |
|
251 |
def check_runtime_arguments(self): |
252 |
... |
253 |
assert self['match'] == self.arguments['match'].value |
254 |
... |
255 |
|
256 |
Non-positional required arguments |
257 |
--------------------------------- |
258 |
|
259 |
By convention, kamaki uses positional arguments for identifiers and |
260 |
non-positional arguments for everything else. By default, non-positional |
261 |
arguments are optional. A non-positional argument can explicitly set to be |
262 |
required at command specification level: |
263 |
|
264 |
.. code-block:: python |
265 |
|
266 |
... |
267 |
|
268 |
@command(_mygrp1_commands) |
269 |
class mygrp1_list_details(_command_init): |
270 |
"""List of details""" |
271 |
|
272 |
arguments = dict( |
273 |
match=ValueArgument( |
274 |
'Filter output to match string', ('-m', --match')) |
275 |
) |
276 |
required = (match, ) |
277 |
|
278 |
A tupple means "all required", while a list notation means "at least one". |
279 |
|
280 |
|
281 |
The main method and command parameters |
282 |
-------------------------------------- |
283 |
|
284 |
The command behavior for each command class is coded in *main*. The |
285 |
parameters of *main* method affect the syntax of the command. In specific:: |
286 |
|
287 |
main(self, param) - obligatory parameter <param> |
288 |
main(self, param=None) - optional parameter [param] |
289 |
main(self, param1, param2=42) - <param1> [param2] |
290 |
main(self, param1____param2) - <param1:param2> |
291 |
main(self, param1____param2=[]) - [param1:param2] |
292 |
main(self, param1____param2__) - <param1[:param2]> |
293 |
main(self, param1____param2__='') - [param1[:param2]] |
294 |
main(self, *args) - arbitary number of params [...] |
295 |
main(self, param1____param2, *args) - <param1:param2> [...] |
296 |
|
297 |
Let's have a look at the command specification class again, and highlight the |
298 |
parts that affect the command syntax: |
299 |
|
300 |
.. code-block:: python |
301 |
:linenos: |
302 |
|
303 |
from kamaki.cli.argument import FlagArgument |
304 |
... |
305 |
|
306 |
_commands = [_mygrp1_commands, _mygrp2_commands] |
307 |
... |
308 |
|
309 |
@command(_mygrp2_commands) |
310 |
class mygrp2_list_all(): |
311 |
"""List all subjects |
312 |
Refers to the subject accessible by current user |
313 |
""" |
314 |
|
315 |
arguments = dict(FlagArgument('detailed list', '-l')) |
316 |
|
317 |
def main(self, reg_exp=None): |
318 |
... |
319 |
|
320 |
The above lines contain the following information: |
321 |
|
322 |
* Namespace and name (line 8): mygrp2 list all |
323 |
* Short (line 9) and long (line 10) description |
324 |
* Parameters (line 15): [reg exp] |
325 |
* Runtime arguments (line 13): [-l] |
326 |
* Runtime arguments help (line 13): detailed list |
327 |
|
328 |
.. tip:: By convention, the main functionality is implemented in a member |
329 |
method called *_run*. This allows the separation between syntax and logic. |
330 |
For example, an external library may need to call a command without caring |
331 |
about its command line behavior. |
332 |
|
333 |
Letting kamaki know |
334 |
------------------- |
335 |
|
336 |
Assume that the command specifications presented so far be stored in a file |
337 |
named *grps.py*. |
338 |
|
339 |
The developer should move the file *grps.py* to *kamaki/cli/commands*, the |
340 |
default place for command specifications |
341 |
|
342 |
These lines should be contained in the kamaki configuration file for a new |
343 |
command specification module to work: |
344 |
:: |
345 |
|
346 |
[global] |
347 |
mygrp1_cli = grps |
348 |
mygrp2_cli = grps |
349 |
|
350 |
or equivalently: |
351 |
|
352 |
.. code-block:: console |
353 |
|
354 |
$ kamaki config set mygrp1_cli grps |
355 |
$ kamaki config set mygrp2_cli grps |
356 |
|
357 |
.. note:: running a command specification from a different path is supported. |
358 |
To achieve this, add a *<group>_cli = </path/to/module>* line in the |
359 |
configure file under the *global* section |
360 |
|
361 |
An example:: |
362 |
|
363 |
[global] |
364 |
mygrp_cli = /another/path/grps.py |
365 |
|
366 |
Summary: create a command set |
367 |
----------------------------- |
368 |
|
369 |
.. code-block:: python |
370 |
|
371 |
# File: grps.py |
372 |
|
373 |
from kamaki.cli.commands import _command_init |
374 |
from kamaki.cli.command_tree import CommandTree |
375 |
from kamaki.cli.argument import ValueArgument, FlagArgument |
376 |
... |
377 |
|
378 |
|
379 |
# Initiallize command trees |
380 |
|
381 |
_mygrp1_commands = CommandTree('mygrp', 'mygrp1 description') |
382 |
_mygrp2_commands = CommandTree('mygrp', 'mygrp2 description') |
383 |
|
384 |
_commands = [_mygrp1_commands, _mygrp2_commands] |
385 |
|
386 |
|
387 |
# Define command specifications |
388 |
|
389 |
|
390 |
@command(_mygrp1_commands) |
391 |
class mygrp1_list(_command_init): |
392 |
"""List mygrp1 objects. |
393 |
There are two versions: short and detailed |
394 |
""" |
395 |
|
396 |
|
397 |
@command(_mygrp1_commands) |
398 |
class mygrp1_list_all(_command_init): |
399 |
"""show a list""" |
400 |
|
401 |
def _run(): |
402 |
... |
403 |
|
404 |
def main(self): |
405 |
self._run() |
406 |
|
407 |
|
408 |
@command(_mygrp1_commands) |
409 |
class mygrp1_list_details(_command_init): |
410 |
"""show list of details""" |
411 |
|
412 |
arguments = dict( |
413 |
match=ValueArgument( |
414 |
'Filter output to match string', ('-m', --match')) |
415 |
) |
416 |
|
417 |
def _run(self): |
418 |
match_value = self['match'] |
419 |
... |
420 |
|
421 |
def main(self): |
422 |
self._run() |
423 |
|
424 |
|
425 |
#The following will also create a mygrp2_list command with no description |
426 |
|
427 |
|
428 |
@command(_mygrp2_commands) |
429 |
class mygrp2_list_all(_command_init): |
430 |
"""list all subjects""" |
431 |
|
432 |
arguments = dict( |
433 |
list=FlagArgument('detailed listing', '-l') |
434 |
) |
435 |
|
436 |
def _run(self, regexp): |
437 |
... |
438 |
if self['list']: |
439 |
... |
440 |
else: |
441 |
... |
442 |
|
443 |
def main(self, regular_expression=None): |
444 |
self._run(regular_expression) |
445 |
|
446 |
|
447 |
@command(_mygrp2_commands) |
448 |
class mygrp2_info(_command_init): |
449 |
"""get information for subject with id""" |
450 |
|
451 |
def _run(self, grp_id, grp_name): |
452 |
... |
453 |
|
454 |
def main(self, id, name=''): |
455 |
self._run(id, name) |