Plugin hooks

Datasette plugins use plugin hooks to customize Datasette's behavior. These hooks are powered by the pluggy plugin system.

Each plugin can implement one or more hooks using the @hookimpl decorator against a function named that matches one of the hooks documented on this page.

When you implement a plugin hook you can accept any or all of the parameters that are documented as being passed to that hook.

For example, you can implement the render_cell plugin hook like this even though the full documented hook signature is render_cell(value, column, table, database, datasette):

@hookimpl
def render_cell(value, column):
    if column == "stars":
        return "*" * int(value)

prepare_connection(conn, database, datasette)

conn - sqlite3 connection object
The connection that is being opened
database - string
The name of the database
datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name)

This hook is called when a new SQLite database connection is created. You can use it to register custom SQL functions, aggregates and collations. For example:

from datasette import hookimpl
import random

@hookimpl
def prepare_connection(conn):
    conn.create_function('random_integer', 2, random.randint)

This registers a SQL function called random_integer which takes two arguments and can be called like this:

select random_integer(1, 10);

Examples: datasette-jellyfish, datasette-jq, datasette-haversine, datasette-rure

prepare_jinja2_environment(env)

env - jinja2 Environment
The template environment that is being prepared

This hook is called with the Jinja2 environment that is used to evaluate Datasette HTML templates. You can use it to do things like register custom template filters, for example:

from datasette import hookimpl

@hookimpl
def prepare_jinja2_environment(env):
    env.filters['uppercase'] = lambda u: u.upper()

You can now use this filter in your custom templates like so:

Table name: {{ table|uppercase }}

extra_template_vars(template, database, table, columns, view_name, request, datasette)

Extra template variables that should be made available in the rendered template context.

template - string
The template that is being rendered, e.g. database.html
database - string or None
The name of the database, or None if the page does not correspond to a database (e.g. the root page)
table - string or None
The name of the table, or None if the page does not correct to a table
columns - list of strings or None
The names of the database columns that will be displayed on this page. None if the page does not contain a table.
view_name - string
The name of the view being displayed. (index, database, table, and row are the most important ones.)
request - object or None
The current HTTP Request object. This can be None if the request object is not available.
datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name)

This hook can return one of three different types:

Dictionary
If you return a dictionary its keys and values will be merged into the template context.
Function that returns a dictionary
If you return a function it will be executed. If it returns a dictionary those values will will be merged into the template context.
Function that returns an awaitable function that returns a dictionary
You can also return a function which returns an awaitable function which returns a dictionary.

Datasette runs Jinja2 in async mode, which means you can add awaitable functions to the template scope and they will be automatically awaited when they are rendered by the template.

Here's an example plugin that adds a "user_agent" variable to the template context containing the current request's User-Agent header:

@hookimpl
def extra_template_vars(request):
    return {
        "user_agent": request.headers.get("user-agent")
    }

This example returns an awaitable function which adds a list of hidden_table_names to the context:

@hookimpl
def extra_template_vars(datasette, database):
    async def hidden_table_names():
        if database:
            db = datasette.databases[database]
            return {"hidden_table_names": await db.hidden_table_names()}
        else:
            return {}
    return hidden_table_names

And here's an example which adds a sql_first(sql_query) function which executes a SQL statement and returns the first column of the first row of results:

@hookimpl
def extra_template_vars(datasette, database):
    async def sql_first(sql, dbname=None):
        dbname = dbname or database or next(iter(datasette.databases.keys()))
        return (await datasette.execute(dbname, sql)).rows[0][0]
    return {"sql_first": sql_first}

You can then use the new function in a template like so:

SQLite version: {{ sql_first("select sqlite_version()") }}

Examples: datasette-search-all, datasette-template-sql

extra_css_urls(template, database, table, columns, view_name, request, datasette)

Same arguments as extra_template_vars(...)

Return a list of extra CSS URLs that should be included on the page. These can take advantage of the CSS class hooks described in Custom pages and templates.

This can be a list of URLs:

from datasette import hookimpl

@hookimpl
def extra_css_urls():
    return [
        'https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css'
    ]

Or a list of dictionaries defining both a URL and an SRI hash:

from datasette import hookimpl

@hookimpl
def extra_css_urls():
    return [{
        'url': 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css',
        'sri': 'sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4',
    }]

This function can also return an awaitable function, useful if it needs to run any async code:

from datasette import hookimpl

@hookimpl
def extra_css_urls(datasette):
    async def inner():
        db = datasette.get_database()
        results = await db.execute("select url from css_files")
        return [r[0] for r in results]

    return inner

Examples: datasette-cluster-map, datasette-vega

extra_js_urls(template, database, table, columns, view_name, request, datasette)

Same arguments as extra_template_vars(...)

This works in the same way as extra_css_urls() but for JavaScript. You can return a list of URLs, a list of dictionaries or an awaitable function that returns those things:

from datasette import hookimpl

@hookimpl
def extra_js_urls():
    return [{
        'url': 'https://code.jquery.com/jquery-3.3.1.slim.min.js',
        'sri': 'sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo',
    }]

You can also return URLs to files from your plugin's static/ directory, if you have one:

from datasette import hookimpl

@hookimpl
def extra_js_urls():
    return [
        '/-/static-plugins/your-plugin/app.js'
    ]

Examples: datasette-cluster-map, datasette-vega

extra_body_script(template, database, table, columns, view_name, request, datasette)

Extra JavaScript to be added to a <script> block at the end of the <body> element on the page.

Same arguments as extra_template_vars(...)

The template, database, table and view_name options can be used to return different code depending on which template is being rendered and which database or table are being processed.

The datasette instance is provided primarily so that you can consult any plugin configuration options that may have been set, using the datasette.plugin_config(plugin_name) method documented above.

The string that you return from this function will be treated as "safe" for inclusion in a <script> block directly in the page, so it is up to you to apply any necessary escaping.

You can also return an awaitable function that returns a string.

Example: datasette-cluster-map

publish_subcommand(publish)

publish - Click publish command group
The Click command group for the datasette publish subcommand

This hook allows you to create new providers for the datasette publish command. Datasette uses this hook internally to implement the default now and heroku subcommands, so you can read their source to see examples of this hook in action.

Let's say you want to build a plugin that adds a datasette publish my_hosting_provider --api_key=xxx mydatabase.db publish command. Your implementation would start like this:

from datasette import hookimpl
from datasette.publish.common import add_common_publish_arguments_and_options
import click


@hookimpl
def publish_subcommand(publish):
    @publish.command()
    @add_common_publish_arguments_and_options
    @click.option(
        "-k",
        "--api_key",
        help="API key for talking to my hosting provider",
    )
    def my_hosting_provider(
        files,
        metadata,
        extra_options,
        branch,
        template_dir,
        plugins_dir,
        static,
        install,
        plugin_secret,
        version_note,
        secret,
        title,
        license,
        license_url,
        source,
        source_url,
        about,
        about_url,
        api_key,
    ):
        # Your implementation goes here

Examples: datasette-publish-fly, datasette-publish-now

render_cell(value, column, table, database, datasette)

Lets you customize the display of values within table cells in the HTML table view.

value - string, integer or None
The value that was loaded from the database
column - string
The name of the column being rendered
table - string or None
The name of the table - or None if this is a custom SQL query
database - string
The name of the database
datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name)

If your hook returns None, it will be ignored. Use this to indicate that your hook is not able to custom render this particular value.

If the hook returns a string, that string will be rendered in the table cell.

If you want to return HTML markup you can do so by returning a jinja2.Markup object.

Datasette will loop through all available render_cell hooks and display the value returned by the first one that does not return None.

Here is an example of a custom render_cell() plugin which looks for values that are a JSON string matching the following format:

{"href": "https://www.example.com/", "label": "Name"}

If the value matches that pattern, the plugin returns an HTML link element:

from datasette import hookimpl
import jinja2
import json


@hookimpl
def render_cell(value):
    # Render {"href": "...", "label": "..."} as link
    if not isinstance(value, str):
        return None
    stripped = value.strip()
    if not stripped.startswith("{") and stripped.endswith("}"):
        return None
    try:
        data = json.loads(value)
    except ValueError:
        return None
    if not isinstance(data, dict):
        return None
    if set(data.keys()) != {"href", "label"}:
        return None
    href = data["href"]
    if not (
        href.startswith("/") or href.startswith("http://")
        or href.startswith("https://")
    ):
        return None
    return jinja2.Markup('<a href="{href}">{label}</a>'.format(
        href=jinja2.escape(data["href"]),
        label=jinja2.escape(data["label"] or "") or "&nbsp;"
    ))

Examples: datasette-render-binary, datasette-render-markdown, datasette-json-html

register_output_renderer(datasette)

datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name)

Registers a new output renderer, to output data in a custom format. The hook function should return a dictionary, or a list of dictionaries, of the following shape:

@hookimpl
def register_output_renderer(datasette):
    return {
        "extension": "test",
        "render": render_demo,
        "can_render": can_render_demo,  # Optional
    }

This will register render_demo to be called when paths with the extension .test (for example /database.test, /database/table.test, or /database/table/row.test) are requested.

render_demo is a Python function. It can be a regular function or an async def render_demo() awaitable function, depending on if it needs to make any asynchronous calls.

can_render_demo is a Python function (or async def function) which acepts the same arguments as render_demo but just returns True or False. It lets Datasette know if the current SQL query can be represented by the plugin - and hence influnce if a link to this output format is displayed in the user interface. If you omit the "can_render" key from the dictionary every query will be treated as being supported by the plugin.

When a request is received, the "render" callback function is called with zero or more of the following arguments. Datasette will inspect your callback function and pass arguments that match its function signature.

datasette - Datasette class
For accessing plugin configuration and executing queries.
columns - list of strings
The names of the columns returned by this query.
rows - list of sqlite3.Row objects
The rows returned by the query.
sql - string
The SQL query that was executed.
query_name - string or None
If this was the execution of a canned query, the name of that query.
database - string
The name of the database.
table - string or None
The table or view, if one is being rendered.
request - Request object
The incoming HTTP request.
view_name - string
The name of the current view being called. index, database, table, and row are the most important ones.

The callback function can return None, if it is unable to render the data, or a Response class that will be returned to the caller.

It can also return a dictionary with the following keys. This format is deprecated as-of Datasette 0.49 and will be removed by Datasette 1.0.

body - string or bytes, optional
The response body, default empty
content_type - string, optional
The Content-Type header, default text/plain
status_code - integer, optional
The HTTP status code, default 200
headers - dictionary, optional
Extra HTTP headers to be returned in the response.

A simple example of an output renderer callback function:

def render_demo():
    return Response.text("Hello World")

Here is a more complex example:

async def render_demo(datasette, columns, rows):
    db = datasette.get_database()
    result = await db.execute("select sqlite_version()")
    first_row = " | ".join(columns)
    lines = [first_row]
    lines.append("=" * len(first_row))
    for row in rows:
        lines.append(" | ".join(row))
    return Response(
        "\n".join(lines),
        content_type="text/plain; charset=utf-8",
        headers={"x-sqlite-version": result.first()[0]}
    )

And here is an example can_render function which returns True only if the query results contain the columns atom_id, atom_title and atom_updated:

def can_render_demo(columns):
    return {"atom_id", "atom_title", "atom_updated"}.issubset(columns)

Examples: datasette-atom, datasette-ics

register_routes()

Register additional view functions to execute for specified URL routes.

Return a list of (regex, view_function) pairs, something like this:

from datasette.utils.asgi import Response
import html


async def hello_from(request):
    name = request.url_vars["name"]
    return Response.html("Hello from {}".format(
        html.escape(name)
    ))


@hookimpl
def register_routes():
    return [
        (r"^/hello-from/(?P<name>.*)$", hello_from)
    ]

The view functions can take a number of different optional arguments. The corresponding argument will be passed to your function depending on its named parameters - a form of dependency injection.

The optional view function arguments are as follows:

datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name), or to execute SQL queries.
request - Request object
The current HTTP Request object.
scope - dictionary
The incoming ASGI scope dictionary.
send - function
The ASGI send function.
receive - function
The ASGI receive function.

The view function can be a regular function or an async def function, depending on if it needs to use any await APIs.

The function can either return a Response class or it can return nothing and instead respond directly to the request using the ASGI send function (for advanced uses only).

Examples: datasette-auth-github, datasette-psutil

register_facet_classes()

Return a list of additional Facet subclasses to be registered.

Warning

The design of this plugin hook is unstable and may change. See issue 830.

Each Facet subclass implements a new type of facet operation. The class should look like this:

class SpecialFacet(Facet):
    # This key must be unique across all facet classes:
    type = "special"

    async def suggest(self):
        # Use self.sql and self.params to suggest some facets
        suggested_facets = []
        suggested_facets.append({
            "name": column, # Or other unique name
            # Construct the URL that will enable this facet:
            "toggle_url": self.ds.absolute_url(
                self.request, path_with_added_args(
                    self.request, {"_facet": column}
                )
            ),
        })
        return suggested_facets

    async def facet_results(self):
        # This should execute the facet operation and return results, again
        # using self.sql and self.params as the starting point
        facet_results = {}
        facets_timed_out = []
        # Do some calculations here...
        for column in columns_selected_for_facet:
            try:
                facet_results_values = []
                # More calculations...
                facet_results_values.append({
                    "value": value,
                    "label": label,
                    "count": count,
                    "toggle_url": self.ds.absolute_url(self.request, toggle_path),
                    "selected": selected,
                })
                facet_results[column] = {
                    "name": column,
                    "results": facet_results_values,
                    "truncated": len(facet_rows_results) > facet_size,
                }
            except QueryInterrupted:
                facets_timed_out.append(column)

        return facet_results, facets_timed_out

See datasette/facets.py for examples of how these classes can work.

The plugin hook can then be used to register the new facet class like this:

@hookimpl
def register_facet_classes():
    return [SpecialFacet]

asgi_wrapper(datasette)

Return an ASGI middleware wrapper function that will be applied to the Datasette ASGI application.

This is a very powerful hook. You can use it to manipulate the entire Datasette response, or even to configure new URL routes that will be handled by your own custom code.

You can write your ASGI code directly against the low-level specification, or you can use the middleware utilites provided by an ASGI framework such as Starlette.

This example plugin adds a x-databases HTTP header listing the currently attached databases:

from datasette import hookimpl
from functools import wraps


@hookimpl
def asgi_wrapper(datasette):
    def wrap_with_databases_header(app):
        @wraps(app)
        async def add_x_databases_header(scope, recieve, send):
            async def wrapped_send(event):
                if event["type"] == "http.response.start":
                    original_headers = event.get("headers") or []
                    event = {
                        "type": event["type"],
                        "status": event["status"],
                        "headers": original_headers + [
                            [b"x-databases",
                            ", ".join(datasette.databases.keys()).encode("utf-8")]
                        ],
                    }
                await send(event)
            await app(scope, recieve, wrapped_send)
        return add_x_databases_header
    return wrap_with_databases_header

Example: datasette-cors

startup(datasette)

This hook fires when the Datasette application server first starts up. You can implement a regular function, for example to validate required plugin configuration:

@hookimpl
def startup(datasette):
    config = datasette.plugin_config("my-plugin") or {}
    assert "required-setting" in config, "my-plugin requires setting required-setting"

Or you can return an async function which will be awaited on startup. Use this option if you need to make any database queries:

@hookimpl
def startup(datasette):
    async def inner():
        db = datasette.get_database()
        if "my_table" not in await db.table_names():
            await db.execute_write("""
                create table my_table (mycol text)
            """, block=True)
    return inner

Potential use-cases:

  • Run some initialization code for the plugin
  • Create database tables that a plugin needs on startup
  • Validate the metadata configuration for a plugin on startup, and raise an error if it is invalid

Note

If you are writing unit tests for a plugin that uses this hook you will need to explicitly call await ds.invoke_startup() in your tests. An example:

@pytest.mark.asyncio
async def test_my_plugin():
    ds = Datasette([], metadata={})
    await ds.invoke_startup()
    # Rest of test goes here

Examples: datasette-saved-queries, datasette-init

canned_queries(datasette, database, actor)

datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name), or to execute SQL queries.
database - string
The name of the database.
actor - dictionary or None
The currently authenticated actor.

Ues this hook to return a dictionary of additional canned query definitions for the specified database. The return value should be the same shape as the JSON described in the canned query documentation.

from datasette import hookimpl

@hookimpl
def canned_queries(datasette, database):
    if database == "mydb":
        return {
            "my_query": {
                "sql": "select * from my_table where id > :min_id"
            }
        }

The hook can alternatively return an awaitable function that returns a list. Here's an example that returns queries that have been stored in the saved_queries database table, if one exists:

from datasette import hookimpl

@hookimpl
def canned_queries(datasette, database):
    async def inner():
        db = datasette.get_database(database)
        if await db.table_exists("saved_queries"):
            results = await db.execute("select name, sql from saved_queries")
            return {result["name"]: {
                "sql": result["sql"]
            } for result in results}
    return inner

The actor parameter can be used to include the currently authenticated actor in your decision. Here's an example that returns saved queries that were saved by that actor:

from datasette import hookimpl

@hookimpl
def canned_queries(datasette, database, actor):
    async def inner():
        db = datasette.get_database(database)
        if actor is not None and await db.table_exists("saved_queries"):
            results = await db.execute(
                "select name, sql from saved_queries where actor_id = :id", {
                    "id": actor["id"]
                }
            )
            return {result["name"]: {
                "sql": result["sql"]
            } for result in results}
    return inner

Example: datasette-saved-queries

actor_from_request(datasette, request)

datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name), or to execute SQL queries.
request - object
The current HTTP Request object.

This is part of Datasette's authentication and permissions system. The function should attempt to authenticate an actor (either a user or an API actor of some sort) based on information in the request.

If it cannot authenticate an actor, it should return None. Otherwise it should return a dictionary representing that actor.

Here's an example that authenticates the actor based on an incoming API key:

from datasette import hookimpl
import secrets

SECRET_KEY = "this-is-a-secret"

@hookimpl
def actor_from_request(datasette, request):
    authorization = request.headers.get("authorization") or ""
    expected = "Bearer {}".format(SECRET_KEY)

    if secrets.compare_digest(authorization, expected):
        return {"id": "bot"}

If you install this in your plugins directory you can test it like this:

$ curl -H 'Authorization: Bearer this-is-a-secret' http://localhost:8003/-/actor.json

Instead of returning a dictionary, this function can return an awaitable function which itself returns either None or a dictionary. This is useful for authentication functions that need to make a database query - for example:

from datasette import hookimpl

@hookimpl
def actor_from_request(datasette, request):
    async def inner():
        token = request.args.get("_token")
        if not token:
            return None
        # Look up ?_token=xxx in sessions table
        result = await datasette.get_database().execute(
            "select count(*) from sessions where token = ?", [token]
        )
        if result.first()[0]:
            return {"token": token}
        else:
            return None

    return inner

Example: datasette-auth-tokens

permission_allowed(datasette, actor, action, resource)

datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name), or to execute SQL queries.
actor - dictionary
The current actor, as decided by actor_from_request(datasette, request).
action - string
The action to be performed, e.g. "edit-table".
resource - string or None
An identifier for the individual resource, e.g. the name of the table.

Called to check that an actor has permission to perform an action on a resource. Can return True if the action is allowed, False if the action is not allowed or None if the plugin does not have an opinion one way or the other.

Here's an example plugin which randomly selects if a permission should be allowed or denied, except for view-instance which always uses the default permission scheme instead.

from datasette import hookimpl
import random

@hookimpl
def permission_allowed(action):
    if action != "view-instance":
        # Return True or False at random
        return random.random() > 0.5
    # Returning None falls back to default permissions

This function can alternatively return an awaitable function which itself returns True, False or None. You can use this option if you need to execute additional database queries using await datasette.execute(...).

Here's an example that allows users to view the admin_log table only if their actor id is present in the admin_users table. It aso disallows arbitrary SQL queries for the staff.db database for all users.

@hookimpl
def permission_allowed(datasette, actor, action, resource):
    async def inner():
        if action == "execute-sql" and resource == "staff":
            return False
        if action == "view-table" and resource == ("staff", "admin_log"):
            if not actor:
                return False
            user_id = actor["id"]
            return await datasette.get_database("staff").execute(
                "select count(*) from admin_users where user_id = :user_id",
                {"user_id": user_id},
            )

    return inner

See built-in permissions for a full list of permissions that are included in Datasette core.

Example: datasette-permissions-sql

register_magic_parameters(datasette)

datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name).

Magic parameters can be used to add automatic parameters to canned queries. This plugin hook allows additional magic parameters to be defined by plugins.

Magic parameters all take this format: _prefix_rest_of_parameter. The prefix indicates which magic parameter function should be called - the rest of the parameter is passed as an argument to that function.

To register a new function, return it as a tuple of (string prefix, function) from this hook. The function you register should take two arguments: key and request, where key is the rest_of_parameter portion of the parameter and request is the current Request object.

This example registers two new magic parameters: :_request_http_version returning the HTTP version of the current request, and :_uuid_new which returns a new UUID:

from uuid import uuid4

def uuid(key, request):
    if key == "new":
        return str(uuid4())
    else:
        raise KeyError

def request(key, request):
    if key == "http_version":
        return request.scope["http_version"]
    else:
        raise KeyError

@hookimpl
def register_magic_parameters(datasette):
    return [
        ("request", request),
        ("uuid", uuid),
    ]

forbidden(datasette, request, message)

datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name), or to execute SQL queries.
request - object
The current HTTP Request object.
message - string
A message hinting at why the request was forbidden.

Plugins can use this to customize how Datasette responds when a 403 Forbidden error occurs - usually because a page failed a permission check, see Permissions.

If a plugin hook wishes to react to the error, it should return a Response object.

This example returns a redirect to a /-/login page:

from datasette import hookimpl
from urllib.parse import urlencode

@hookimpl
def forbidden(request, message):
    return Response.redirect("/-/login?=" + urlencode({"message": message}))

The function can alternatively return an awaitable function if it needs to make any asynchronous method calls. This example renders a template:

from datasette import hookimpl
from datasette.utils.asgi import Response

@hookimpl
def forbidden(datasette):
    async def inner():
        return Response.html(await datasette.render_template("forbidden.html"))

    return inner