October 14, 2021·4 min read·engineeringsqlitedatasettepluginsmetadata

Datasette 0.59 lets plugins extend its command line

A command-registration hook brings plugin operations into the Datasette CLI. Column descriptions also give publishers a place to explain what their SQLite fields mean.

A plugin that adds a web feature may also need an operation someone runs before serving data. Datasette 0.59 gives such operations a place in its existing command line. The October 14 release adds register_commands, a hook through which an installed plugin can register additional commands. A caller can invoke a plugin operation as datasette mycommand, using the same command group that starts and manages the application.

The addition is useful because command discovery and plugin loading are related concerns. An extension does not have to ask its users to remember a separate executable name for every operation. It can register with the command group already associated with the data-publishing tool. This does not automatically give commands a database connection or a web request, but it gives their arguments and help text an established interface.

I value SQLite tools that keep the surrounding operational work proportionate to the database. A small command extension fits that preference when it makes a concrete operation easier to find. The hook's loading rules are as relevant as its decorator syntax.

Registration receives the command group

The plugin-hook documentation in the 0.59 tag defines register_commands(cli). Its argument is Datasette's root Click command group. A plugin adds commands through that group, with Click responsible for their command-line arguments, options, and help behavior.

The following minimal example uses the documented registration pattern. Its deliberately simple operation makes the ownership of registration visible without introducing another library or database behavior.

import click
from datasette import hookimpl

@hookimpl
def register_commands(cli):
    @cli.command()
    @click.argument("label")
    def identify(label):
        "Print the supplied dataset label."
        click.echo(label)

Once packaged and installed as a Datasette plugin, this registers datasette identify LABEL. The command's docstring supplies help text. A plugin can register more than one command through multiple calls to cli.command(), so the hook does not constrain each package to one operation.

The release documentation gives a more practical example that opens supplied paths with Python's SQLite module and attempts to read sqlite_master. It reports a Click error when SQLite rejects a database. That demonstrates a useful boundary, the plugin chooses its database operation, while Click handles its invocation and error presentation. Registration alone does not specify what the operation validates.

Installation determines whether the command exists

The hook has an explicit restriction. These plugins cannot use Datasette's --plugins-dir mechanism. They must be installed into the same virtual environment as Datasette. The documentation shows an editable installation for a plugin under development, using pip install -e with the plugin directory.

This rule affects deployment packaging. Copying a Python file into a directory used for other one-off plugins is insufficient to make its command available through this hook. The environment that runs the datasette executable needs the installed plugin package. Command discovery therefore belongs in the installation's dependency definition.

The restriction also helps explain why a command cannot simply depend on a running web instance. A caller may be asking for help or running an operation before any server starts. The new hook takes the CLI group as its input. It does not receive the request or application arguments supplied to other hooks.

Other plugin hooks change separately in this release. register_routes can receive an optional Datasette argument, and render_cell can return an awaitable function that executes SQL queries. Those additions support work inside the application. They should not be treated as arguments or capabilities automatically supplied to the command-registration hook.

I prefer that explicit separation because it keeps a plugin's startup assumptions visible. A command that needs a database path should declare one. A command that performs preparation work should be usable without implying that an HTTP request already exists.

Column descriptions carry meaning beside the data

The other notable addition is metadata for individual column descriptions. The tagged metadata documentation places a columns mapping inside a table's metadata. Its keys are column names, and its values describe those columns.

{
  "databases": {
    "observations": {
      "tables": {
        "samples": {
          "columns": {
            "station": "Identifier assigned by the source dataset",
            "reading": "Value reported by the source instrument"
          }
        }
      }
    }
  }
}

These illustrative descriptions do not change stored values or impose constraints. Datasette displays them at the top of the table page and in each column's cog menu. They explain the fields where a reader encounters them, instead of requiring a separate document whose column names must be matched manually.

The distinction from a schema type matters. A column declared as an integer still needs an explanation if its values represent external identifiers. A description can record that meaning without implying arithmetic is appropriate. Metadata can also explain a source convention without changing the SQLite table used to publish it.

Both October additions extend the publishing tool around the database. Installed plugins can provide discoverable preparation or inspection commands, while publishers can attach field explanations to table pages. The SQL engine still stores and queries the values. Datasette supplies the command entry points and the contextual text that help people operate and interpret that data.