datasette-plugins
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
$ インストール
git clone https://github.com/datasette/skill ~/.claude/skills/skill// tip: Run this command in your terminal to install the skill
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:
--template-dirargument- Plugin templates
- 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
