Command-Line with click

click is another popular package for creating beautiful CLI.

One option to use trafaret_config is to define new click argument type based, which expects path to an existing configuration file plus trafaret rules.

Create cli.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import click
import trafaret_config as traf_cfg
import trafaret as t
import time

CONFIG_TRAFARET = t.Dict({t.Key("host"): t.String(), t.Key("port"): t.Int()})


class TrafaretYaml(click.Path):
    """Configuration read from YAML file checked against trafaret rules."""
    name = "trafaret yaml file"

    def __init__(self, trafaret):
        self.trafaret = trafaret
        super().__init__(
            exists=True, file_okay=True, dir_okay=False, readable=True)

    def convert(self, value, param, ctx):
        cfg_file = super().convert(value, param, ctx)
        try:
            return traf_cfg.read_and_validate(cfg_file, self.trafaret)
        except traf_cfg.ConfigError as e:
            msg = "\n".join(str(err) for err in e.errors)
            self.fail("\n" + msg)


@click.group()
def cli():
    pass


@cli.command()
@click.argument("config", type=TrafaretYaml(CONFIG_TRAFARET))
def validate(config):
    """Validate configuration file structure."""
    click.echo("OK: Configuration is valid.")


@cli.command()
@click.argument("config", type=TrafaretYaml(CONFIG_TRAFARET))
def run(config):
    """Run web application.
    """
    # Start the application
    host = config["host"]
    port = config["port"]
    print("Would like to run the app at {host}:{port}...".format(
        host=host, port=port))
    time.sleep(5)
    print("..done.")


if __name__ == "__main__":
    cli()

CONFIG_TRAFARET is sample trafaret rule for our config file, which may look like config.yaml:

host: localhost
port: 1234

class TrafaretYaml(click.Path) defines a class for new click type.

def cli(): defines top level command to run and it has two subcommands:

Subcommand validating the configuration file is really simple:

@cli.command()
@click.argument("config", type=TrafaretYaml(CONFIG_TRAFARET))
def validate(config):
    """Validate configuration file structure."""
    click.echo("OK: Configuration is valid.")

using type=TrafaretYaml it implicitly expects path to config file and at the same time prescribes trafaret rules for it’s content.

def run(): goes one step further and uses the configuration values.

Sample usage

First explore the main command:

$ python cli.py
Usage: cli.py [OPTIONS] COMMAND [ARGS]...

Options:
--help  Show this message and exit.

Commands:
run       Run web application.
validate  Validate configuration file structure.

It provides two subcommands.

Subcommand validate allows configuration file validation:

$ python cli.py validate cfg.yaml
OK: Configuration is valid.

If the config file does not exist:

$ python cli.py run cfg-not-here.yaml
Usage: cli.py run [OPTIONS] CONFIG

Error: Invalid value for "config": Path "cfg-not-here.yaml" does not exist.

it reports this problem.

If port number has value 1234a, it uses trafaret rules to report the problem:

$ python cli.py va lidate cfg.yaml
Usage: cli.py validate [OPTIONS] CONFIG

Error: Invalid value for "config":
cfg.yaml:2: port: value can't be converted to int

If all is fine, it allows running the applicaiton:

$ python cli.py run cfg.yaml
Would like to run the app at localhost:1234...
..done.

Hint: add subcommand init printing sample configuration file content.