Statistics
| Branch: | Tag: | Revision:

root / docs / developers / adding-commands.rst @ 16d7b9ff

History | View | Annotate | Download (12.8 kB)

1
Adding Commands
2
===============
3

    
4
Kamaki commands are implemented as python classes, decorated with a special
5
decorator called *command*. This decorator is a method of *kamaki.cli* that
6
adds a new command in a *CommandTree* structure. A *CommandTree* (package
7
*kamaki.cli.commant_tree*) is a data structure used by kamaki to manage command
8
namespaces.
9

    
10
For demonstration purposes, the following set of kamaki commands will be
11
implemented in this document::
12

    
13
    mygrp1 list all                         //show a list
14
    mygrp1 list details [--match=<>]        //show list of details
15
    mygrp2 list all [regular expression] [-l]       //list all subjects
16
    mygrp2 info <id> [name]      //get information for subject with id
17

    
18
There are two command groups to implement i.e., *mygrp1* and *mygrp2*,
19
containing two commands each (*list_all*, *list_details* and *list_all*, *info*
20
respectively). To avoid ambiguities, command names are prefixed with the
21
command group they belong to, e.g., *mygrp1_list_all* and *mygrp2_list_all*.
22
The underscore is used to separate command namespaces.
23

    
24
The first command (*mygrp1_list_all*) has the simplest possible syntax: no
25
parameters, no runtime arguments. The second accepts an optional runtime argument with a value. The third features an optional parameter and an optional
26
runtime flag argument. The last is an example of a command with an obligatory
27
and an optional parameter.
28

    
29
Examples of the expected behavior in one-command mode:
30

    
31
.. code-block:: console
32

    
33
    $kamaki mygrp1
34
        mygrp1 description
35

    
36
        Options
37
         - - - -
38
        list
39
    $ kamaki mygrp1 list
40

    
41
        Options
42
         - - - -
43
        all        show a list
44
        details     show a list of details
45
    $ kamaki mygrp1 list all
46
        ... (a mygrp1_list_all instance runs) ...
47
    $ kamaki mygrp2 list all 'Z[.]' -l
48
        ... (a mygrp2_list_all instance runs) ...
49
    $
50

    
51
The CommandTree structure
52
-------------------------
53

    
54
CommandTree manages a command by its namespace. Each command is stored in
55
a tree path, where each node is a name. A leaf is the end term of a namespace and contains a pointer to the command class to be executed.
56

    
57
Here is an example from the actual kamaki command structure, where the commands
58
*file upload*, *file list* and *file info* are represented as shown bellow::
59

    
60
    - file
61
    ''''''''|- info
62
            |- list
63
            |- upload
64

    
65
Now, let's load the showcase example on CommandTrees::
66

    
67
    - mygrp1
68
    ''''''''|- list
69
            '''''''|- all
70
                   |- details
71

    
72
    - mygrp2
73
    ''''''''|- list
74
            '''''''|- all
75
            |- info
76

    
77
Each command group should be stored on a different CommandTree.
78

    
79
For that reason, command specification modules should contain a list of CommandTree objects, named *_commands*. This mechanism allows any interface
80
application to load the list of commands from the *_commands* array.
81

    
82
The first name of the command path and a description (name, description) are needed to initializeg a CommandTree:
83

    
84
.. code-block:: python
85

    
86
    _mygrp1_commands = CommandTree('mygrp', 'mygrp1 description')
87
    _mygrp2_commands = CommandTree('mygrp', 'mygrp2 description')
88

    
89
    _commands = [_mygrp1_commands, _mygrp2_commands]
90

    
91

    
92
The command decorator
93
---------------------
94

    
95
All commands are specified by subclasses of *kamaki.cli.commands._command_init*
96
These classes are called "command specifications".
97

    
98
The *command* decorator mines all the information needed to build a namespace
99
from a command specification::
100

    
101
    class code  --->  command()  -->  updated CommandTree structure
102

    
103
Kamaki interfaces make use of the CommandTree structure. Optimizations are
104
possible by using special parameters on the command decorator method.
105

    
106
.. code-block:: python
107

    
108
    def command(cmd_tree, prefix='', descedants_depth=None):
109
    """Load a class as a command
110

    
111
        :param cmd_tree: is the CommandTree to be updated with a new command
112

    
113
        :param prefix: of the commands allowed to be inserted ('' for all)
114

    
115
        :param descedants_depth: is the depth of the tree descendants of the
116
            prefix command.
117
    """
118

    
119
Creating a new command specification set
120
----------------------------------------
121

    
122
A command specification developer should create a new module (python file) with
123
one command specification class per command. Each class should be decorated
124
with *command*.
125

    
126
.. code-block:: python
127

    
128
    ...
129
    _commands = [_mygrp1_commands, _mygrp2_commands]
130

    
131
    @command(_mygrp1_commands)
132
    class mygrp1_list_all():
133
        ...
134

    
135
    ...
136

    
137
A list of CommandTree structures must exist in the module scope, with the name
138
*_commands*. Different CommandTree objects correspond to different command
139
groups.
140

    
141
Set command description
142
-----------------------
143

    
144
The description of each command is the first line of the class commend. The
145
following declaration of *mygrp2-info* command has a "*get information for
146
subject with id*" description.
147

    
148
.. code-block:: python
149

    
150
    ...
151
    @command(_mygrp2_commands)
152
    class mygrp2_info():
153
        """get information for subject with id
154
        Anything from this point and bellow constitutes the long description
155
        Please, mind the indentation, pep8 is not forgiving.
156
        """
157
        ...
158

    
159
Description placeholders
160
------------------------
161

    
162
There is possible to create an empty command, that can act as a description
163
placeholder. For example, the *mygrp1_list* namespace does not correspond to an
164
executable command, but it can have a helpful description. In that case, create
165
a command specification class with a command and no code:
166

    
167
.. code-block:: python
168

    
169
    @command(_mygrp1_commands)
170
    class mygrp1_list():
171
        """List mygrp1 objects.
172
        There are two versions: short and detailed
173
        """
174

    
175
.. warning:: A command specification class with no description is invalid and
176
    will cause an error.
177

    
178
Declare run-time argument
179
-------------------------
180

    
181
A special argument mechanism allows the definition of run-time arguments. This
182
mechanism is based on argparse and is designed to simplify argument definitions
183
when specifying commands.
184

    
185
Some basic argument types are defined at the
186
`argument module <code.html#module-kamaki.cli.argument>`_, but it is not
187
a bad idea to extent these classes in order to achieve specialized type
188
checking and syntax control. Still, in most cases, the argument types of the
189
argument package are enough for most cases.
190

    
191
To declare a run-time argument on a specific command, the specification class
192
should contain a dict called *arguments* , where Argument objects are stored.
193
Each argument object is a run-time argument. Syntax checking happens at client
194
level, while the type checking is implemented in the Argument code (e.g.,
195
IntArgument checks if the value is an int).
196

    
197
.. code-block:: python
198

    
199
    from kamaki.cli.argument import ValueArgument
200
    ...
201

    
202
    @command(_mygrp1_commands)
203
    class mygrp1_list_details():
204
        """list of details"""
205

    
206
        def __init__(self, global_args={}):
207
            global_args['match'] = ValueArgument(
208
                'Filter results to match string',
209
                ('-m', '--match'))
210
            self.arguments = global_args
211

    
212
or more usually and elegantly:
213

    
214
.. code-block:: python
215

    
216
    from kamaki.cli.argument import ValueArgument
217
    
218
    @command(_mygrp1_commands)
219
    class mygrp1_list_details():
220
    """List of details"""
221

    
222
        arguments = dict(
223
            match=ValueArgument(
224
                'Filter output to match string', ('-m', --match'))
225
        )
226

    
227
Accessing run-time arguments
228
----------------------------
229

    
230
To access run-time arguments, users can use the *_command_init* interface,
231
which implements *__item__* accessors to handle run-time argument values. In
232
other words, one may get the value of an argument with *self[<argument>]*.
233

    
234
.. code-block:: python
235

    
236
    from kamaki.cli.argument import ValueArgument
237
    from kamaki.cli.commands import _command_init
238
    
239
    @command(_mygrp1_commands)
240
    class mygrp1_list_details(_command_init):
241
        """List of details"""
242

    
243
        arguments = dict(
244
            match=ValueArgument(
245
                'Filter output to match string', ('-m', --match'))
246
        )
247

    
248
        def check_runtime_arguments(self):
249
            ...
250
            assert self['match'] == self.arguments['match'].value
251
            ...
252

    
253
The main method and command parameters
254
--------------------------------------
255

    
256
The command behavior for each command class is coded in *main*. The
257
parameters of *main* method affect the syntax of the command. In specific::
258

    
259
    main(self, param)                   - obligatory parameter <param>
260
    main(self, param=None)              - optional parameter [param]
261
    main(self, param1, param2=42)       - <param1> [param2]
262
    main(self, param1____param2)        - <param1:param2>
263
    main(self, param1____param2=[])     - [param1:param2]
264
    main(self, param1____param2__)      - <param1[:param2]>
265
    main(self, param1____param2__='')   - [param1[:param2]]
266
    main(self, *args)                   - arbitary number of params [...]
267
    main(self, param1____param2, *args) - <param1:param2> [...]
268

    
269
Let's have a look at the command specification class again, and highlight the
270
parts that affect the command syntax:
271

    
272
.. code-block:: python
273
    :linenos:
274

    
275
    from kamaki.cli.argument import FlagArgument
276
    ...
277

    
278
    _commands = [_mygrp1_commands, _mygrp2_commands]
279
    ...
280

    
281
    @command(_mygrp2_commands)
282
    class mygrp2_list_all():
283
        """List all subjects
284
        Refers to the subject accessible by current user
285
        """
286

    
287
        arguments = dict(FlagArgument('detailed list', '-l'))
288

    
289
        def main(self, reg_exp=None):
290
            ...
291

    
292
The above lines contain the following information:
293

    
294
* Namespace and name (line 8): mygrp2 list all
295
* Short (line 9) and long (line 10) description
296
* Parameters (line 15): [reg exp]
297
* Runtime arguments (line 13): [-l]
298
* Runtime arguments help (line 13): detailed list
299

    
300
.. tip:: It is suggested to code the main functionality in a member method
301
    called *_run*. This allows the separation between syntax and logic. For
302
    example, an external library may need to call a command without caring
303
    about its command line behavior.
304

    
305
Letting kamaki know
306
-------------------
307

    
308
Kamaki will load a command specification *only* if it is set as a configurable
309
option. To demonstrate this, let the command specifications coded above be
310
stored in a file named *grps.py*.
311

    
312
The developer should move the file *grps.py* to *kamaki/cli/commands*, the
313
default place for command specifications
314

    
315
These lines should be contained in the kamaki configuration file for a new
316
command specification module to work:
317
::
318

    
319
    [global]
320
    mygrp1_cli = grps
321
    mygrp2_cli = grps
322

    
323
or equivalently:
324

    
325
.. code-block:: console
326

    
327
    $ kamaki config set mygrp1_cli grps
328
    $ kamaki config set mygrp2_cli grps
329

    
330
.. note:: running a command specification from a different path is supported.
331
    To achieve this, add a *<group>_cli = </path/to/module>* line in the
332
    configure file under the *global* section.
333
::
334

    
335
    [global]
336
    mygrp_cli = /another/path/grps.py
337

    
338
Summary: create a command set
339
-----------------------------
340

    
341
.. code-block:: python
342

    
343
    #  File: grps.py
344

    
345
    from kamaki.cli.commands import _command_init
346
    from kamaki.cli.command_tree import CommandTree
347
    from kamaki.cli.argument import ValueArgument, FlagArgument
348
    ...
349

    
350

    
351
    #  Initiallize command trees
352

    
353
    _mygrp1_commands = CommandTree('mygrp', 'mygrp1 description')
354
    _mygrp2_commands = CommandTree('mygrp', 'mygrp2 description')
355

    
356
    _commands = [_mygrp1_commands, _mygrp2_commands]
357

    
358

    
359
    #  Define command specifications
360

    
361

    
362
    @command(_mygrp1_commands)
363
    class mygrp1_list(_command_init):
364
        """List mygrp1 objects.
365
        There are two versions: short and detailed
366
        """
367

    
368

    
369
    @command(_mygrp1_commands)
370
    class mygrp1_list_all(_command_init):
371
        """show a list"""
372

    
373
        def _run():
374
            ...
375

    
376
        def main(self):
377
            self._run()
378

    
379

    
380
    @command(_mygrp1_commands)
381
    class mygrp1_list_details(_command_init):
382
        """show list of details"""
383

    
384
        arguments = dict(
385
            match=ValueArgument(
386
                'Filter output to match string', ('-m', --match'))
387
        )
388

    
389
        def _run(self):
390
            match_value = self['match']
391
            ...
392

    
393
        def main(self):
394
        self._run()
395

    
396

    
397
    #The following will also create a mygrp2_list command with no description
398

    
399

    
400
    @command(_mygrp2_commands)
401
    class mygrp2_list_all(_command_init):
402
        """list all subjects"""
403

    
404
        arguments = dict(
405
            list=FlagArgument('detailed listing', '-l')
406
        )
407

    
408
        def _run(self, regexp):
409
            ...
410
            if self['list']:
411
                ...
412
            else:
413
                ...
414

    
415
        def main(self, regular_expression=None):
416
            self._run(regular_expression)
417

    
418

    
419
    @command(_mygrp2_commands)
420
    class mygrp2_info(_command_init):
421
        """get information for subject with id"""
422

    
423
        def _run(self, grp_id, grp_name):
424
            ...
425

    
426
        def main(self, id, name=''):
427
            self._run(id, name)