Claude-skill-registry 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/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/datasette-plugins" ~/.claude/skills/majiayu000-claude-skill-registry-datasette-plugins && rm -rf "$T"
skills/data/datasette-plugins/SKILL.mdDatasette 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 |
|---|---|
| Register custom SQL functions |
| Add custom URL routes |
| Initialize on server start |
| Customize cell display |
| Add template variables |
| Custom authentication |
| 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:
- Global feature/-/my-feature
- Database-specific/dbname/-/my-feature
- Table-specific/dbname/tablename/-/my-feature
Static Assets & Templates
Static files in
static/ are served at:
/-/static-plugins/PLUGIN_NAME/filename.js
Templates in
templates/ override Datasette defaults. Priority:
argument--template-dir- 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