Most web projects will need at some point a command line interface (CLI) for administrative purposes. I am wrapping up my current idea of how to approach this for a typical Pyramid project based on the library Click.

The role of Click

Click provides a bunch of decorators which help to easily create consistent and nice-to-use user interfaces on the command line.

Minimal example

The following excerpt shows the basic principle:

import click

@click.command()
def my_command():
    pass

You would typically register your command as a console_script inside your setup.py. The Click documentation is quite comprehensive and contains more useful examples.

Using sub-commands

One very nice feature of Click is the support for sub-commands. I envision my CLI to work somewhat like the following examples:

  • my-project

    This should show a help message and overview of available commands.

  • my-project --config=local.ini

    For all commands it should be possible to specify the configuration to use. Depending on the project, this is either a mandatory option or it will have a default value.

    Especially for projects where most things are based on environment variables, the need for the parameter config is probably not given.

  • my-project --config=local.ini command --option=value

    Each command could have its own bunch of options, depending on the need.

The Pyramid side

On this end, I would like the Pyramid framework to be initialized and also logging to be set up. This seems to be the safest bet for the default case.

This means, the following pieces of the API should be used:

  • pyramid.paster.bootstrap()

    This will initialize the Pyramid framework itself. It needs the path to the INI file.

  • pyramid.paster.setup_logging()

    This function will set up the package logging from the standard library based on the given INI file.

All details of these API calls are documented in the API documentation of pyramid.paster.

Putting the fragments together

The idea is to use the base command for bringing up the framework and then attach sub-commands to it for the functionality.

The base command

First of all, define the base command via click.group:

@click.group()
@click.argument('ini')
@click.pass_context
def cli(ctx, ini):
    setup_logging(ini)
    env = bootstrap(ini)
    ctx.obj = env

This should configure logging and bring up the Pyramid framework. ctx.obj is a way to make objects available to subcommands. env is a dict which contains relevant objects like a Request instance, the WSGI application object and similar things. Note that it may be useful to pass in a custom Request instance if you want to construct URLs in your tasks.

An example command

An example command could then be created as follows:

@cli.command()
@click.option('--option', help='Example option')
@click.pass_context
def command(ctx, option):
    """
    Documentation of the example command. It will be available when running
    it with "--help".
    """
    pass

Open questions

Should there be a CLI interface at all?

I am not quite sure about this question, my best guess at the moment is that it depends on the project.

If the project has already a web API for administrative purposes, it could very well make sense to just extend it with the needed functionality. Pyramid itself has a command prequest which allows to "send" a request from the command line. In case this is good enough, I would avoid to create an additional CLI interface.

Opt-out of the framework initialization

Bootstrapping the full Pyramid framework might be way too much for some commands. It might be worth to look for a way how to opt out of the automatic setup on a per sub-command basis.

Conclusions

On the first glance this looks like a quite nice fit to me. The base command could be placed in a central base package like project.cli and others could use it from there to build specific commands. This would lead to a nice decoupling of the base setup and the individual logic of the sub-command.

Follow up

There is a follow up about the Pyramid integration based on the Configurator object.


Comments

comments powered by Disqus