Statistics
| Branch: | Tag: | Revision:

root / docs / developers / adding-commands.rst @ 1b70db0d

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)