Claude Code Plugins

Community-maintained marketplace

Feedback

datasette-plugins

@datasette/skill
10
0

Writing Datasette plugins using Python and the pluggy plugin system. Use when Claude needs to: (1) Create a new Datasette plugin, (2) Implement plugin hooks like prepare_connection, register_routes, render_cell, etc., (3) Add custom SQL functions, (4) Create custom output renderers, (5) Add authentication or permissions logic, (6) Extend Datasette's UI with menus, actions, or templates, (7) Package a plugin for distribution on PyPI

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name datasette-plugins
description Writing Datasette plugins using Python and the pluggy plugin system. Use when Claude needs to: (1) Create a new Datasette plugin, (2) Implement plugin hooks like prepare_connection, register_routes, render_cell, etc., (3) Add custom SQL functions, (4) Create custom output renderers, (5) Add authentication or permissions logic, (6) Extend Datasette's UI with menus, actions, or templates, (7) Package a plugin for distribution on PyPI

Datasette Plugin Development

Overview

Datasette plugins extend Datasette's functionality using Python and the pluggy plugin system. Plugins can add SQL functions, custom routes, authentication, UI elements, and more.

Quick Start: One-off Plugin

Create plugins/my_plugin.py:

from datasette import hookimpl

@hookimpl
def prepare_connection(conn):
    conn.create_function("hello_world", 0, lambda: "Hello world!")

Run with: datasette serve mydb.db --plugins-dir=plugins/

Installable Plugin Structure

For distributable plugins, use this structure:

datasette-my-plugin/
├── pyproject.toml
├── datasette_my_plugin/
│   ├── __init__.py      # Plugin implementation
│   ├── static/          # Optional: JS/CSS files
│   └── templates/       # Optional: Jinja2 templates
└── tests/
    └── test_plugin.py

pyproject.toml

[project]
name = "datasette-my-plugin"
version = "0.1.0"
description = "My Datasette plugin"
requires-python = ">=3.10"
dependencies = ["datasette"]
[dependency-groups]
dev = [
    "pytest",
    "pytest-asyncio"
]

[project.entry-points.datasette]
my_plugin = "datasette_my_plugin"

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

Core Plugin Hooks

See references/hooks.md for complete hook documentation.

Most Common Hooks

Hook Purpose
prepare_connection(conn, database, datasette) Register custom SQL functions
register_routes(datasette) Add custom URL routes
startup(datasette) Initialize on server start
render_cell(row, value, column, table, database, datasette, request) Customize cell display
extra_template_vars(...) Add template variables
actor_from_request(datasette, request) Custom authentication
permission_allowed(datasette, actor, action, resource) Custom permissions

Example: Custom SQL Function

from datasette import hookimpl
import hashlib

@hookimpl
def prepare_connection(conn):
    conn.create_function("md5", 1, lambda s: hashlib.md5(s.encode()).hexdigest())

Example: Custom Route

from datasette import hookimpl, Response

@hookimpl
def register_routes():
    return [
        (r"^/-/my-page$", my_page_view),
    ]

async def my_page_view(datasette, request):
    return Response.html("<h1>My Custom Page</h1>")

Example: Startup Hook

@hookimpl
def startup(datasette):
    async def inner():
        db = datasette.get_database()
        await db.execute_write("""
            CREATE TABLE IF NOT EXISTS my_table (id INTEGER PRIMARY KEY, data TEXT)
        """)
    return inner

Plugin Configuration

Plugins read configuration from datasette.yaml:

plugins:
  datasette-my-plugin:
    option1: value1
    option2: value2

Access in plugin:

@hookimpl
def startup(datasette):
    config = datasette.plugin_config("datasette-my-plugin") or {}
    my_option = config.get("option1", "default")

Secret Configuration

Use environment variables:

plugins:
  datasette-my-plugin:
    api_key:
      $env: MY_API_KEY

Or files:

plugins:
  datasette-my-plugin:
    api_key:
      $file: /secrets/api-key

Testing Plugins

from datasette.app import Datasette
import pytest

@pytest.mark.asyncio
async def test_plugin_installed():
    datasette = Datasette(memory=True)
    response = await datasette.client.get("/-/plugins.json")
    assert response.status_code == 200
    plugins = {p["name"] for p in response.json()}
    assert "datasette-my-plugin" in plugins

@pytest.mark.asyncio
async def test_custom_route():
    datasette = Datasette(memory=True)
    response = await datasette.client.get("/-/my-page")
    assert response.status_code == 200
    assert "My Custom Page" in response.text

Run tests: pytest

Response Types

from datasette import Response

# HTML response
Response.html("<h1>Hello</h1>")

# JSON response
Response.json({"key": "value"})

# Text response
Response.text("Plain text")

# Redirect
Response.redirect("/other-page")

# Custom response
Response(body, content_type="text/plain", status=200, headers={})

URL Design

Use /-/ prefix to avoid conflicts with database names:

  • /-/my-feature - Global feature
  • /dbname/-/my-feature - Database-specific
  • /dbname/tablename/-/my-feature - Table-specific

Static Assets & Templates

Static files in static/ are served at: /-/static-plugins/PLUGIN_NAME/filename.js

Templates in templates/ override Datasette defaults. Priority:

  1. --template-dir argument
  2. Plugin templates
  3. Datasette defaults

Common Patterns

Adding Menu Items

@hookimpl
def menu_links(datasette, actor):
    return [{"href": "/-/my-page", "label": "My Feature"}]

Table Actions

@hookimpl
def table_actions(datasette, actor, database, table):
    return [{"href": f"/{database}/{table}/-/action", "label": "My Action"}]

Custom Output Renderer

@hookimpl
def register_output_renderer(datasette):
    return {
        "extension": "csv",
        "render": render_csv,
    }

async def render_csv(datasette, columns, rows):
    # Return Response object
    pass

Event Tracking

@hookimpl
def track_event(datasette, event):
    print(f"Event: {event.name}, Actor: {event.actor}")

Debugging

Enable hook tracing:

DATASETTE_TRACE_PLUGINS=1 datasette mydb.db

Key Imports

from datasette import hookimpl, Response
from datasette.app import Datasette
from datasette.filters import FilterArguments
from datasette.permissions import Action, Resource, PermissionSQL
import markupsafe  # For safe HTML in render_cell