datasette-plugin-writer

0
0
Source

Guide for writing Datasette plugins. This skill should be used when users want to create or develop plugins for Datasette, including information about plugin hooks, the cookiecutter template, database APIs, request/response handling, and plugin configuration.

Install

mkdir -p .claude/skills/datasette-plugin-writer && curl -L -o skill.zip "https://mcp.directory/api/skills/download/5761" && unzip -o skill.zip -d .claude/skills/datasette-plugin-writer && rm skill.zip

Installs to .claude/skills/datasette-plugin-writer

About this skill

Writing Datasette Plugins

Use this skill to build plugins for Datasette, the open source multi-tool for exploring and publishing data.

Quick Start with Cookiecutter Template

Start a new plugin using the datasette-plugin cookiecutter template with newline-delimited variables:

echo "plugin_name
description of plugin
plugin-hyphenated-name
plugin_underscored_name
github_username
Author Name
y
y" | uvx cookiecutter gh:simonw/datasette-plugin

Example for a plugin called "my-cool-plugin":

echo "my cool plugin
A plugin that does cool things
my-cool-plugin
my_cool_plugin
username
Your Name
y
y" | uvx cookiecutter gh:simonw/datasette-plugin

The last two y responses enable static/ and templates/ directories.

After creating the plugin:

cd datasette-my-cool-plugin
python -m venv venv
source venv/bin/activate
pip install -e '.[test]'
datasette plugins  # Verify plugin is visible
python -m pytest   # Run tests

Plugin Structure

A typical plugin structure:

datasette-my-plugin/
├── datasette_my_plugin/
│   ├── __init__.py        # Plugin hooks go here
│   ├── static/            # Optional: CSS, JavaScript
│   └── templates/         # Optional: Custom templates
├── tests/
│   └── test_my_plugin.py
├── setup.py or pyproject.toml
└── README.md

Essential Plugin Hooks

prepare_connection(conn, database, datasette)

Register custom SQL functions. Called when SQLite connections are created:

from datasette import hookimpl

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

register_routes(datasette)

Add custom URL routes. Return list of (regex, view_function) pairs:

from datasette import hookimpl, Response

async def my_page(request):
    return Response.html("<h1>Hello!</h1>")

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

View functions can accept: datasette, request, scope, send, receive.

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

Customize how table cell values are displayed:

from datasette import hookimpl
import markupsafe

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

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

Add variables to template context:

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

Can also return async functions for database queries.

table_actions(datasette, actor, database, table, request)

Add menu items to table pages:

@hookimpl
def table_actions(datasette, database, table):
    return [{
        "href": datasette.urls.path(f"/-/export/{database}/{table}"),
        "label": "Export this table",
        "description": "Download as CSV"
    }]

actor_from_request(datasette, request)

Implement authentication. Return actor dict or None:

@hookimpl
def actor_from_request(request):
    token = request.args.get("_token")
    if token == "secret":
        return {"id": "user123", "name": "Alice"}

Can return async function for database lookups.

permission_allowed(datasette, actor, action, resource)

Control permissions. Return True (allow), False (deny), or None (no opinion):

@hookimpl
def permission_allowed(actor, action, resource):
    if action == "execute-sql" and actor and actor.get("id") == "admin":
        return True

Request and Response Objects

Request Object

Available in many plugin hooks:

request.method          # "GET" or "POST"
request.url             # Full URL
request.path            # Path without query string
request.full_path       # Path with query string
request.query_string    # Query string without ?
request.args            # MultiParams object for query params
request.args.get("key") # Get single param value
request.args.getlist("key")  # Get list of values
request.headers         # Dict of headers (lowercase keys)
request.cookies         # Dict of cookies
request.actor           # Current authenticated actor or None
request.url_vars        # Variables from URL regex

# Async methods:
body = await request.post_body()      # Raw POST body as bytes
form_vars = await request.post_vars() # Form data as dict

Response Object

Create responses in view functions:

from datasette.utils.asgi import Response

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

# JSON response
return Response.json({"status": "ok"})

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

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

# Custom response
return Response(
    body="Content",
    status=200,
    headers={"X-Custom": "value"},
    content_type="text/plain"
)

# Set cookies
response = Response.html("<h1>Hello</h1>")
response.set_cookie("session", datasette.sign({"id": "123"}, "cookie"))

Database API

Access databases in plugins:

# Get database object
db = datasette.get_database("mydb")  # Named database
db = datasette.get_database()        # First database

# Execute read query
results = await db.execute("SELECT * FROM mytable WHERE id = ?", [123])
for row in results:
    print(row["column_name"])

# Query properties
results.rows         # List of Row objects
results.columns      # List of column names
results.truncated    # True if results were truncated
results.first()      # First row or None
results.single_value()  # Single value if query returned one

# Execute write query
await db.execute_write(
    "INSERT INTO mytable (name) VALUES (?)",
    ["value"]
)

# Execute multiple writes
await db.execute_write_many(
    "INSERT INTO mytable (id, name) VALUES (?, ?)",
    [(1, "Alice"), (2, "Bob")]
)

# Execute function with write connection
def insert_and_count(conn):
    conn.execute("INSERT INTO mytable (name) VALUES (?)", ["Alice"])
    return conn.execute("SELECT COUNT(*) FROM mytable").fetchone()[0]

count = await db.execute_write_fn(insert_and_count)

# Introspection
tables = await db.table_names()
views = await db.view_names()
columns = await db.table_columns("mytable")
exists = await db.table_exists("mytable")

Plugin Configuration

Users configure plugins in datasette.yaml:

plugins:
  datasette-my-plugin:
    api_key: secret123
    enabled: true

Or per-database:

databases:
  mydb:
    plugins:
      datasette-my-plugin:
        setting: value

Access in plugin code:

config = datasette.plugin_config("datasette-my-plugin")
api_key = config.get("api_key") if config else None

# With database/table context
config = datasette.plugin_config(
    "datasette-my-plugin",
    database="mydb",
    table="mytable"
)

Configuration lookup: table → database → instance level.

Static Assets and Templates

Static Files

Place in static/ directory, reference with:

# In Python
url = datasette.urls.static_plugins("datasette_my_plugin", "app.js")

# In templates
<script src="{{ urls.static_plugins('datasette_my_plugin', 'app.js') }}"></script>

Templates

Place in templates/ directory. Override Datasette templates:

  • database.html - Database page
  • table.html - Table page
  • row.html - Row page
  • query.html - Query page

Access template functions:

{{ csrftoken() }}                    {# CSRF token for forms #}
{{ urls.instance() }}                {# Homepage URL #}
{{ urls.database("mydb") }}          {# Database URL #}
{{ urls.table("mydb", "mytable") }}  {# Table URL #}

Common Patterns

Add a custom SQL function

@hookimpl
def prepare_connection(conn):
    import hashlib
    def md5(text):
        return hashlib.md5(text.encode()).hexdigest()
    conn.create_function("md5", 1, md5)

Add a custom page

@hookimpl
def register_routes(datasette):
    async def stats_page(request):
        db = datasette.get_database()
        tables = await db.table_names()
        return Response.html(
            await datasette.render_template(
                "stats.html",
                {"tables": tables},
                request=request
            )
        )

    return [(r"^/-/stats$", stats_page)]

Render custom output format

@hookimpl
def register_output_renderer(datasette):
    def render_csv(columns, rows):
        import csv, io
        output = io.StringIO()
        writer = csv.writer(output)
        writer.writerow(columns)
        writer.writerows(rows)
        return Response(
            output.getvalue(),
            content_type="text/csv"
        )

    return {
        "extension": "csv",
        "render": render_csv
    }

Check permissions

@hookimpl
def register_routes(datasette):
    async def admin_page(request):
        # Check if user has permission
        allowed = await datasette.permission_allowed(
            request.actor,
            "admin-page",
            default=False
        )
        if not allowed:
            from datasette import Forbidden
            raise Forbidden("Admin access required")

        return Response.html("<h1>Admin Page</h1>")

    return [(r"^/-/admin$", admin_page)]

URL Design

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

  • /-/my-plugin - Instance-level page
  • /dbname/-/my-plugin - Database-level page
  • /dbname/table/-/my-plugin - Table-level page

Build URLs with base_url support:

datasette.urls.path("/-/my-page")
datasette.urls.database("mydb")
datasette.urls.table("mydb", "mytable")

Testing Plugins

Create tests in tests/test_my_plugin.py:

from datasette.app import Datasette
import pytest

@pytest.mark.asyncio
async def test_my_plugin():
    datasette = Datasette()
    await datasette.invoke_startup()

    # Test with client
    r

---

*Content truncated.*

You might also like

flutter-development

aj-geddes

Build beautiful cross-platform mobile apps with Flutter and Dart. Covers widgets, state management with Provider/BLoC, navigation, API integration, and material design.

643969

drawio-diagrams-enhanced

jgtolentino

Create professional draw.io (diagrams.net) diagrams in XML format (.drawio files) with integrated PMP/PMBOK methodologies, extensive visual asset libraries, and industry-standard professional templates. Use this skill when users ask to create flowcharts, swimlane diagrams, cross-functional flowcharts, org charts, network diagrams, UML diagrams, BPMN, project management diagrams (WBS, Gantt, PERT, RACI), risk matrices, stakeholder maps, or any other visual diagram in draw.io format. This skill includes access to custom shape libraries for icons, clipart, and professional symbols.

591705

ui-ux-pro-max

nextlevelbuilder

"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient."

318398

godot

bfollington

This skill should be used when working on Godot Engine projects. It provides specialized knowledge of Godot's file formats (.gd, .tscn, .tres), architecture patterns (component-based, signal-driven, resource-based), common pitfalls, validation tools, code templates, and CLI workflows. The `godot` command is available for running the game, validating scripts, importing resources, and exporting builds. Use this skill for tasks involving Godot game development, debugging scene/resource files, implementing game systems, or creating new Godot components.

339397

nano-banana-pro

garg-aayush

Generate and edit images using Google's Nano Banana Pro (Gemini 3 Pro Image) API. Use when the user asks to generate, create, edit, modify, change, alter, or update images. Also use when user references an existing image file and asks to modify it in any way (e.g., "modify this image", "change the background", "replace X with Y"). Supports both text-to-image generation and image-to-image editing with configurable resolution (1K default, 2K, or 4K for high resolution). DO NOT read the image file first - use this skill directly with the --input-image parameter.

451339

fastapi-templates

wshobson

Create production-ready FastAPI projects with async patterns, dependency injection, and comprehensive error handling. Use when building new FastAPI applications or setting up backend API projects.

304231

Stay ahead of the MCP ecosystem

Get weekly updates on new skills and servers.