Markdown Converter
Agent skill for markdown-converter
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
Loading actions...
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.
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/
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
[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"
See references/hooks.md for complete hook documentation.
| 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 |
from datasette import hookimpl
import hashlib
@hookimpl
def prepare_connection(conn):
conn.create_function("md5", 1, lambda s: hashlib.md5(s.encode()).hexdigest())
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>")
@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
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")
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
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
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={})
Use /-/ prefix to avoid conflicts with database names:
/-/my-feature - Global feature/dbname/-/my-feature - Database-specific/dbname/tablename/-/my-feature - Table-specificStatic files in static/ are served at:
/-/static-plugins/PLUGIN_NAME/filename.js
Templates in templates/ override Datasette defaults. Priority:
--template-dir argument@hookimpl
def menu_links(datasette, actor):
return [{"href": "/-/my-page", "label": "My Feature"}]
@hookimpl
def table_actions(datasette, actor, database, table):
return [{"href": f"/{database}/{table}/-/action", "label": "My Action"}]
@hookimpl
def register_output_renderer(datasette):
return {
"extension": "csv",
"render": render_csv,
}
async def render_csv(datasette, columns, rows):
# Return Response object
pass
@hookimpl
def track_event(datasette, event):
print(f"Event: {event.name}, Actor: {event.actor}")
Enable hook tracing:
DATASETTE_TRACE_PLUGINS=1 datasette mydb.db
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