.. _plugin_hooks: Plugin hooks ============ Datasette :ref:`plugins ` use *plugin hooks* to customize Datasette's behavior. These hooks are powered by the `pluggy `__ plugin system. Each plugin can implement one or more hooks using the ``@hookimpl`` decorator against a function named that matches one of the hooks documented on this page. When you implement a plugin hook you can accept any or all of the parameters that are documented as being passed to that hook. For example, you can implement the ``render_cell`` plugin hook like this even though the full documented hook signature is ``render_cell(row, value, column, table, pks, database, datasette, request)``: .. code-block:: python @hookimpl def render_cell(value, column): if column == "stars": return "*" * int(value) .. contents:: List of plugin hooks :local: :class: this-will-duplicate-information-and-it-is-still-useful-here .. _plugin_hook_prepare_connection: prepare_connection(conn, database, datasette) --------------------------------------------- ``conn`` - sqlite3 connection object The connection that is being opened ``database`` - string The name of the database ``datasette`` - :ref:`internals_datasette` You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)`` This hook is called when a new SQLite database connection is created. You can use it to `register custom SQL functions `_, aggregates and collations. For example: .. code-block:: python from datasette import hookimpl import random @hookimpl def prepare_connection(conn): conn.create_function( "random_integer", 2, random.randint ) This registers a SQL function called ``random_integer`` which takes two arguments and can be called like this:: select random_integer(1, 10); ``prepare_connection()`` hooks are not called for Datasette's :ref:`internal database `. Examples: `datasette-jellyfish `__, `datasette-jq `__, `datasette-haversine `__, `datasette-rure `__ .. _plugin_hook_write_wrapper: write_wrapper(datasette, database, request, transaction) -------------------------------------------------------- ``datasette`` - :ref:`internals_datasette` You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``. ``database`` - string The name of the database being written to. ``request`` - :ref:`internals_request` or ``None`` The HTTP request that triggered this write, if available. This will be ``None`` for writes that do not originate from an HTTP request (e.g. writes triggered by plugins during startup). ``transaction`` - bool ``True`` if the write will be wrapped in a database transaction. Return a generator function that accepts a ``conn`` argument (a SQLite connection object) and optionally a ``track_event`` argument. The generator should ``yield`` exactly once. Code before the ``yield`` runs before the write function executes; code after the ``yield`` runs after it completes. The result of the write function is sent back through the ``yield``, so you can capture it with ``result = yield``. If the write function raises an exception, it is thrown into the generator so you can handle it with a ``try`` / ``except`` around the ``yield``. If your generator accepts ``track_event``, you can call ``track_event(event)`` to queue an event that will be dispatched via :ref:`datasette.track_event() ` after the write commits successfully. Events are discarded if the write raises an exception. Return ``None`` to skip wrapping for this particular write. This example logs every write operation: .. code-block:: python from datasette import hookimpl @hookimpl def write_wrapper(datasette, database, request): def wrapper(conn): print(f"Before write to {database}") result = yield print(f"After write to {database}") return wrapper This more advanced example uses the SQLite authorizer callback to block writes to a specific table for non-admin users: .. code-block:: python import sqlite3 from datasette import hookimpl WRITE_ACTIONS = ( sqlite3.SQLITE_INSERT, sqlite3.SQLITE_UPDATE, sqlite3.SQLITE_DELETE, ) @hookimpl def write_wrapper(datasette, database, request): actor = None if request: actor = request.actor if actor and actor.get("id") == "admin": return None def wrapper(conn): def authorizer( action, arg1, arg2, db_name, trigger ): if ( action in WRITE_ACTIONS and arg1 == "protected_table" ): return sqlite3.SQLITE_DENY return sqlite3.SQLITE_OK conn.set_authorizer(authorizer) try: yield finally: conn.set_authorizer(None) return wrapper The ``conn`` object passed to the generator is the same connection that the write function will use. Because the generator and the write function execute together in a single call on the write thread, any state you set on the connection (authorizers, pragmas, temporary tables) is visible to the write and can be cleaned up afterwards. When multiple plugins implement ``write_wrapper``, they are nested following pluggy's default calling convention. .. _plugin_hook_prepare_jinja2_environment: prepare_jinja2_environment(env, datasette) ------------------------------------------ ``env`` - jinja2 Environment The template environment that is being prepared ``datasette`` - :ref:`internals_datasette` You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)`` This hook is called with the Jinja2 environment that is used to evaluate Datasette HTML templates. You can use it to do things like `register custom template filters `_, for example: .. code-block:: python from datasette import hookimpl @hookimpl def prepare_jinja2_environment(env): env.filters["uppercase"] = lambda u: u.upper() You can now use this filter in your custom templates like so:: Table name: {{ table|uppercase }} This function can return an awaitable function if it needs to run any async code. Examples: `datasette-edit-templates `_ .. _plugin_page_extras: Page extras ----------- These plugin hooks can be used to affect the way HTML pages for different Datasette interfaces are rendered. .. _plugin_hook_extra_template_vars: extra_template_vars(template, database, table, columns, view_name, request, datasette) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Extra template variables that should be made available in the rendered template context. ``template`` - string The template that is being rendered, e.g. ``database.html`` ``database`` - string or None The name of the database, or ``None`` if the page does not correspond to a database (e.g. the root page) ``table`` - string or None The name of the table, or ``None`` if the page does not correct to a table ``columns`` - list of strings or None The names of the database columns that will be displayed on this page. ``None`` if the page does not contain a table. ``view_name`` - string The name of the view being displayed. (``index``, ``database``, ``table``, and ``row`` are the most important ones.) ``request`` - :ref:`internals_request` or None The current HTTP request. This can be ``None`` if the request object is not available. ``datasette`` - :ref:`internals_datasette` You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)`` This hook can return one of three different types: Dictionary If you return a dictionary its keys and values will be merged into the template context. Function that returns a dictionary If you return a function it will be executed. If it returns a dictionary those values will will be merged into the template context. Function that returns an awaitable function that returns a dictionary You can also return a function which returns an awaitable function which returns a dictionary. Datasette runs Jinja2 in `async mode `__, which means you can add awaitable functions to the template scope and they will be automatically awaited when they are rendered by the template. Here's an example plugin that adds a ``"user_agent"`` variable to the template context containing the current request's User-Agent header: .. code-block:: python @hookimpl def extra_template_vars(request): return {"user_agent": request.headers.get("user-agent")} This example returns an awaitable function which adds a list of ``hidden_table_names`` to the context: .. code-block:: python @hookimpl def extra_template_vars(datasette, database): async def hidden_table_names(): if database: db = datasette.databases[database] return { "hidden_table_names": await db.hidden_table_names() } else: return {} return hidden_table_names And here's an example which adds a ``sql_first(sql_query)`` function which executes a SQL statement and returns the first column of the first row of results: .. code-block:: python @hookimpl def extra_template_vars(datasette, database): async def sql_first(sql, dbname=None): dbname = ( dbname or database or next(iter(datasette.databases.keys())) ) result = await datasette.execute(dbname, sql) return result.rows[0][0] return {"sql_first": sql_first} You can then use the new function in a template like so:: SQLite version: {{ sql_first("select sqlite_version()") }} Examples: `datasette-search-all `_, `datasette-template-sql `_ .. _plugin_hook_extra_css_urls: extra_css_urls(template, database, table, columns, view_name, request, datasette) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This takes the same arguments as :ref:`extra_template_vars(...) ` Return a list of extra CSS URLs that should be included on the page. These can take advantage of the CSS class hooks described in :ref:`customization`. This can be a list of URLs: .. code-block:: python from datasette import hookimpl @hookimpl def extra_css_urls(): return [ "https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" ] Or a list of dictionaries defining both a URL and an `SRI hash `_: .. code-block:: python @hookimpl def extra_css_urls(): return [ { "url": "https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css", "sri": "sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4", } ] This function can also return an awaitable function, useful if it needs to run any async code: .. code-block:: python @hookimpl def extra_css_urls(datasette): async def inner(): db = datasette.get_database() results = await db.execute( "select url from css_files" ) return [r[0] for r in results] return inner Examples: `datasette-cluster-map `_, `datasette-vega `_ .. _plugin_hook_extra_js_urls: extra_js_urls(template, database, table, columns, view_name, request, datasette) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This takes the same arguments as :ref:`extra_template_vars(...) ` This works in the same way as ``extra_css_urls()`` but for JavaScript. You can return a list of URLs, a list of dictionaries or an awaitable function that returns those things: .. code-block:: python from datasette import hookimpl @hookimpl def extra_js_urls(): return [ { "url": "https://code.jquery.com/jquery-3.3.1.slim.min.js", "sri": "sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo", } ] You can also return URLs to files from your plugin's ``static/`` directory, if you have one: .. code-block:: python @hookimpl def extra_js_urls(): return ["/-/static-plugins/your-plugin/app.js"] Note that ``your-plugin`` here should be the hyphenated plugin name - the name that is displayed in the list on the ``/-/plugins`` debug page. If your code uses `JavaScript modules `__ you should include the ``"module": True`` key. See :ref:`configuration_reference_css_js` for more details. .. code-block:: python @hookimpl def extra_js_urls(): return [ { "url": "/-/static-plugins/your-plugin/app.js", "module": True, } ] Examples: `datasette-cluster-map `_, `datasette-vega `_ .. _plugin_hook_extra_body_script: extra_body_script(template, database, table, columns, view_name, request, datasette) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Extra JavaScript to be added to a ```` element: .. code-block:: python @hookimpl def extra_body_script(): return { "module": True, "script": "console.log('Your JavaScript goes here...')", } This will add the following to the end of your page: .. code-block:: html Example: `datasette-cluster-map `_ .. _plugin_hook_publish_subcommand: publish_subcommand(publish) --------------------------- ``publish`` - Click publish command group The Click command group for the ``datasette publish`` subcommand This hook allows you to create new providers for the ``datasette publish`` command. Datasette uses this hook internally to implement the default ``cloudrun`` and ``heroku`` subcommands, so you can read `their source `_ to see examples of this hook in action. Let's say you want to build a plugin that adds a ``datasette publish my_hosting_provider --api_key=xxx mydatabase.db`` publish command. Your implementation would start like this: .. code-block:: python from datasette import hookimpl from datasette.publish.common import ( add_common_publish_arguments_and_options, ) import click @hookimpl def publish_subcommand(publish): @publish.command() @add_common_publish_arguments_and_options @click.option( "-k", "--api_key", help="API key for talking to my hosting provider", ) def my_hosting_provider( files, metadata, extra_options, branch, template_dir, plugins_dir, static, install, plugin_secret, version_note, secret, title, license, license_url, source, source_url, about, about_url, api_key, ): ... Examples: `datasette-publish-fly `_, `datasette-publish-vercel `_ .. _plugin_hook_render_cell: render_cell(row, value, column, table, pks, database, datasette, request, column_type) -------------------------------------------------------------------------------------- Lets you customize the display of values within table cells in the HTML table view. ``row`` - ``sqlite.Row`` The SQLite row object that the value being rendered is part of ``value`` - string, integer, float, bytes or None The value that was loaded from the database ``column`` - string The name of the column being rendered ``table`` - string or None The name of the table - or ``None`` if this is a custom SQL query ``pks`` - list of strings The primary key column names for the table being rendered. For tables without an explicitly defined primary key, this will be ``["rowid"]``. For custom SQL queries and views (where ``table`` is ``None``), this will be an empty list ``[]``. ``database`` - string The name of the database ``datasette`` - :ref:`internals_datasette` You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``, or to execute SQL queries. ``request`` - :ref:`internals_request` The current request object ``column_type`` - :ref:`ColumnType ` subclass instance or None The :ref:`ColumnType ` subclass instance assigned to this column (with ``.config`` populated), or ``None`` if no column type is assigned. You can access ``column_type.name``, ``column_type.config``, etc. If a column has a :ref:`column type ` assigned and that column type's ``render_cell`` method returns a non-``None`` value, it will take priority over this plugin hook. If your hook returns ``None``, it will be ignored. Use this to indicate that your hook is not able to custom render this particular value. If the hook returns a string, that string will be rendered in the table cell. If you want to return HTML markup you can do so by returning a ``jinja2.Markup`` object. You can also return an awaitable function which returns a value. Datasette will loop through all available ``render_cell`` hooks and display the value returned by the first one that does not return ``None``. Here is an example of a custom ``render_cell()`` plugin which looks for values that are a JSON string matching the following format:: {"href": "https://www.example.com/", "label": "Name"} If the value matches that pattern, the plugin returns an HTML link element: .. code-block:: python from datasette import hookimpl import markupsafe import json @hookimpl def render_cell(value): # Render {"href": "...", "label": "..."} as link if not isinstance(value, str): return None stripped = value.strip() if not ( stripped.startswith("{") and stripped.endswith("}") ): return None try: data = json.loads(value) except ValueError: return None if not isinstance(data, dict): return None if set(data.keys()) != {"href", "label"}: return None href = data["href"] if not ( href.startswith("/") or href.startswith("http://") or href.startswith("https://") ): return None return markupsafe.Markup( '{label}'.format( href=markupsafe.escape(data["href"]), label=markupsafe.escape(data["label"] or "") or " ", ) ) Examples: `datasette-render-binary `_, `datasette-render-markdown `__, `datasette-json-html `__ .. _plugin_register_output_renderer: register_output_renderer(datasette) ----------------------------------- ``datasette`` - :ref:`internals_datasette` You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)`` Registers a new output renderer, to output data in a custom format. The hook function should return a dictionary, or a list of dictionaries, of the following shape: .. code-block:: python @hookimpl def register_output_renderer(datasette): return { "extension": "test", "render": render_demo, "can_render": can_render_demo, # Optional } This will register ``render_demo`` to be called when paths with the extension ``.test`` (for example ``/database.test``, ``/database/table.test``, or ``/database/table/row.test``) are requested. ``render_demo`` is a Python function. It can be a regular function or an ``async def render_demo()`` awaitable function, depending on if it needs to make any asynchronous calls. ``can_render_demo`` is a Python function (or ``async def`` function) which accepts the same arguments as ``render_demo`` but just returns ``True`` or ``False``. It lets Datasette know if the current SQL query can be represented by the plugin - and hence influence if a link to this output format is displayed in the user interface. If you omit the ``"can_render"`` key from the dictionary every query will be treated as being supported by the plugin. When a request is received, the ``"render"`` callback function is called with zero or more of the following arguments. Datasette will inspect your callback function and pass arguments that match its function signature. ``datasette`` - :ref:`internals_datasette` For accessing plugin configuration and executing queries. ``columns`` - list of strings The names of the columns returned by this query. ``rows`` - list of ``sqlite3.Row`` objects The rows returned by the query. ``sql`` - string The SQL query that was executed. ``query_name`` - string or None If this was the execution of a :ref:`stored query `, the name of that query. ``database`` - string The name of the database. ``table`` - string or None The table or view, if one is being rendered. ``request`` - :ref:`internals_request` The current HTTP request. ``error`` - string or None If an error occurred this string will contain the error message. ``truncated`` - bool or None If the query response was truncated - for example a SQL query returning more than 1,000 results where pagination is not available - this will be ``True``. ``view_name`` - string The name of the current view being called. ``index``, ``database``, ``table``, and ``row`` are the most important ones. The callback function can return ``None``, if it is unable to render the data, or a :ref:`internals_response` that will be returned to the caller. It can also return a dictionary with the following keys. This format is **deprecated** as-of Datasette 0.49 and will be removed by Datasette 1.0. ``body`` - string or bytes, optional The response body, default empty ``content_type`` - string, optional The Content-Type header, default ``text/plain`` ``status_code`` - integer, optional The HTTP status code, default 200 ``headers`` - dictionary, optional Extra HTTP headers to be returned in the response. An example of an output renderer callback function: .. code-block:: python def render_demo(): return Response.text("Hello World") Here is a more complex example: .. code-block:: python async def render_demo(datasette, columns, rows): db = datasette.get_database() result = await db.execute("select sqlite_version()") first_row = " | ".join(columns) lines = [first_row] lines.append("=" * len(first_row)) for row in rows: lines.append(" | ".join(row)) return Response( "\n".join(lines), content_type="text/plain; charset=utf-8", headers={"x-sqlite-version": result.first()[0]}, ) And here is an example ``can_render`` function which returns ``True`` only if the query results contain the columns ``atom_id``, ``atom_title`` and ``atom_updated``: .. code-block:: python def can_render_demo(columns): return { "atom_id", "atom_title", "atom_updated", }.issubset(columns) Examples: `datasette-atom `_, `datasette-ics `_, `datasette-geojson `__, `datasette-copyable `__ .. _plugin_register_routes: register_routes(datasette) -------------------------- ``datasette`` - :ref:`internals_datasette` You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)`` Register additional view functions to execute for specified URL routes. Return a list of ``(regex, view_function)`` pairs, something like this: .. code-block:: python from datasette import hookimpl, Response import html async def hello_from(request): name = request.url_vars["name"] return Response.html( "Hello from {}".format(html.escape(name)) ) @hookimpl def register_routes(): return [(r"^/hello-from/(?P.*)$", hello_from)] The view functions can take a number of different optional arguments. The corresponding argument will be passed to your function depending on its named parameters - a form of dependency injection. The optional view function arguments are as follows: ``datasette`` - :ref:`internals_datasette` You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)``, or to execute SQL queries. ``request`` - :ref:`internals_request` The current HTTP request. ``scope`` - dictionary The incoming ASGI scope dictionary. ``send`` - function The ASGI send function. ``receive`` - function The ASGI receive function. The view function can be a regular function or an ``async def`` function, depending on if it needs to use any ``await`` APIs. The function can either return a :ref:`internals_response` or it can return nothing and instead respond directly to the request using the ASGI ``send`` function (for advanced uses only). It can also raise the ``datasette.NotFound`` exception to return a 404 not found error, or the ``datasette.Forbidden`` exception for a 403 forbidden. See :ref:`writing_plugins_designing_urls` for tips on designing the URL routes used by your plugin. Examples: `datasette-auth-github `__, `datasette-psutil `__ .. _plugin_hook_register_commands: register_commands(cli) ---------------------- ``cli`` - the root Datasette `Click command group `__ Use this to register additional CLI commands Register additional CLI commands that can be run using ``datsette yourcommand ...``. This provides a mechanism by which plugins can add new CLI commands to Datasette. This example registers a new ``datasette verify file1.db file2.db`` command that checks if the provided file paths are valid SQLite databases: .. code-block:: python from datasette import hookimpl import click import sqlite3 @hookimpl def register_commands(cli): @cli.command() @click.argument( "files", type=click.Path(exists=True), nargs=-1 ) def verify(files): "Verify that files can be opened by Datasette" for file in files: conn = sqlite3.connect(str(file)) try: conn.execute("select * from sqlite_master") except sqlite3.DatabaseError: raise click.ClickException( "Invalid database: {}".format(file) ) The new command can then be executed like so:: datasette verify fixtures.db Help text (from the docstring for the function plus any defined Click arguments or options) will become available using:: datasette verify --help Plugins can register multiple commands by making multiple calls to the ``@cli.command()`` decorator. Consult the `Click documentation `__ for full details on how to build a CLI command, including how to define arguments and options. Note that ``register_commands()`` plugins cannot used with the :ref:`--plugins-dir mechanism ` - they need to be installed into the same virtual environment as Datasette using ``pip install``. Provided it has a ``pyproject.toml`` file (see :ref:`writing_plugins_packaging`) you can run ``pip install`` directly against the directory in which you are developing your plugin like so:: pip install -e path/to/my/datasette-plugin Examples: `datasette-auth-passwords `__, `datasette-verify `__ .. _plugin_register_facet_classes: register_facet_classes() ------------------------ Return a list of additional Facet subclasses to be registered. .. warning:: The design of this plugin hook is unstable and may change. See `issue 830 `__. Each Facet subclass implements a new type of facet operation. The class should look like this: .. code-block:: python class SpecialFacet(Facet): # This key must be unique across all facet classes: type = "special" async def suggest(self): # Use self.sql and self.params to suggest some facets suggested_facets = [] suggested_facets.append( { "name": column, # Or other unique name # Construct the URL that will enable this facet: "toggle_url": self.ds.absolute_url( self.request, path_with_added_args( self.request, {"_facet": column} ), ), } ) return suggested_facets async def facet_results(self): # This should execute the facet operation and return results, again # using self.sql and self.params as the starting point facet_results = [] facets_timed_out = [] facet_size = self.get_facet_size() # Do some calculations here... for column in columns_selected_for_facet: try: facet_results_values = [] # More calculations... facet_results_values.append( { "value": value, "label": label, "count": count, "toggle_url": self.ds.absolute_url( self.request, toggle_path ), "selected": selected, } ) facet_results.append( { "name": column, "results": facet_results_values, "truncated": len(facet_rows_results) > facet_size, } ) except QueryInterrupted: facets_timed_out.append(column) return facet_results, facets_timed_out See `datasette/facets.py `__ for examples of how these classes can work. The plugin hook can then be used to register the new facet class like this: .. code-block:: python @hookimpl def register_facet_classes(): return [SpecialFacet] .. _plugin_register_actions: register_actions(datasette) --------------------------- If your plugin needs to register actions that can be checked with Datasette's new resource-based permission system, return a list of those actions from this hook. Actions define what operations can be performed on resources (like viewing a table, executing SQL, or custom plugin actions). .. code-block:: python from datasette import hookimpl from datasette.permissions import Action, Resource class DocumentCollectionResource(Resource): """A collection of documents.""" name = "document-collection" parent_class = None def __init__(self, collection: str): super().__init__(parent=collection, child=None) @classmethod async def resources_sql( cls, datasette, actor=None ) -> str: return """ SELECT collection_name AS parent, NULL AS child FROM document_collections """ class DocumentResource(Resource): """A document in a collection.""" name = "document" parent_class = DocumentCollectionResource def __init__(self, collection: str, document: str): super().__init__(parent=collection, child=document) @classmethod async def resources_sql( cls, datasette, actor=None ) -> str: return """ SELECT collection_name AS parent, document_id AS child FROM documents """ @hookimpl def register_actions(datasette): return [ Action( name="list-documents", abbr="ld", description="List documents in a collection", resource_class=DocumentCollectionResource, ), Action( name="view-document", abbr="vdoc", description="View document", resource_class=DocumentResource, ), Action( name="edit-document", abbr="edoc", description="Edit document", resource_class=DocumentResource, ), ] The fields of the ``Action`` dataclass are as follows: ``name`` - string The name of the action, e.g. ``view-document``. This should be unique across all plugins. ``abbr`` - string or None An abbreviation of the action, e.g. ``vdoc``. This is optional. Since this needs to be unique across all installed plugins it's best to choose carefully or omit it entirely (same as setting it to ``None``.) ``description`` - string or None A human-readable description of what the action allows you to do. ``resource_class`` - type[Resource] or None The Resource subclass that defines what kind of resource this action applies to. Omit this (or set to ``None``) for global actions that apply only at the instance level with no associated resources (like ``debug-menu`` or ``permissions-debug``). Your Resource subclass must: - Define a ``name`` class attribute (e.g., ``"document"``) - Define a ``parent_class`` class attribute (``None`` for top-level resources like databases, or the parent ``Resource`` subclass for child resources) - Implement an async ``resources_sql(cls, datasette, actor=None)`` classmethod that returns SQL returning all resources as ``(parent, child)`` columns - Have an ``__init__`` method that accepts appropriate parameters and calls ``super().__init__(parent=..., child=...)`` .. _plugin_resources_sql: The ``resources_sql(datasette, actor)`` method ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``resources_sql()`` classmethod returns a SQL query that lists all resources of that type that exist in the system. It can be async because Datasette calls it with ``await``, and it receives the current ``datasette`` instance plus an optional ``actor`` argument. This query is used by Datasette to efficiently check permissions across multiple resources at once. When a user requests a list of resources (like tables, documents, or other entities), Datasette uses this SQL to: 1. Get all resources of this type from your data catalog 2. Combine it with permission rules from the ``permission_resources_sql`` hook 3. Use SQL joins and filtering to determine which resources the actor can access 4. Return only the permitted resources The SQL query **must** return exactly two columns: - ``parent`` - The parent identifier (e.g., database name, collection name), or ``NULL`` for top-level resources - ``child`` - The child identifier (e.g., table name, document ID), or ``NULL`` for parent-only resources For example, if you're building a document management plugin with collections and documents stored in a ``documents`` table, your ``resources_sql()`` might look like: .. code-block:: python @classmethod async def resources_sql(cls, datasette, actor=None) -> str: return """ SELECT collection_name AS parent, document_id AS child FROM documents """ This tells Datasette "here's how to find all documents in the system - look in the documents table and get the collection name and document ID for each one." The permission system then uses this query along with rules from plugins to determine which documents each user can access, all efficiently in SQL rather than loading everything into Python. .. _plugin_register_column_types: register_column_types(datasette) -------------------------------- Return a list of :ref:`ColumnType ` **subclasses** (not instances) to register custom column types. Column types define how values in specific columns are rendered, validated, and transformed. .. code-block:: python from datasette import hookimpl from datasette.column_types import ColumnType, SQLiteType import markupsafe class ColorColumnType(ColumnType): name = "color" description = "CSS color value" sqlite_types = (SQLiteType.TEXT,) async def render_cell( self, value, column, table, database, datasette, request, ): if value: return markupsafe.Markup( '' "{color}" ).format(color=markupsafe.escape(value)) return None async def validate(self, value, datasette): if value and not value.startswith("#"): return "Color must start with #" return None async def transform_value(self, value, datasette): # Normalize to uppercase if isinstance(value, str): return value.upper() return value @hookimpl def register_column_types(datasette): return [ColorColumnType] Each ``ColumnType`` subclass must define the following class attributes: ``name`` - string Unique identifier for the column type, e.g. ``"color"``. Must be unique across all plugins. ``description`` - string Human-readable label, e.g. ``"CSS color value"``. ``sqlite_types`` - tuple of ``SQLiteType`` values, optional Restrict assignments of this column type to columns with matching SQLite types, e.g. ``(SQLiteType.TEXT,)``. If omitted, the column type can be assigned to any column. And the following methods, all optional: ``render_cell(self, value, column, table, database, datasette, request)`` Return an HTML string to render this cell value, or ``None`` to fall through to the default ``render_cell`` plugin hook chain. When a column type provides rendering, it takes priority over the ``render_cell`` plugin hook. ``validate(self, value, datasette)`` Validate a value before it is written via the insert, update, or upsert API endpoints. Return ``None`` if valid, or a string error message if invalid. Null values and empty strings skip validation. ``transform_value(self, value, datasette)`` Transform a value before it appears in JSON API output. Return the transformed value. The default implementation returns the value unchanged. Per-column configuration is available via ``self.config`` in all methods. When a column type is looked up for a specific column (via :ref:`get_column_type ` or :ref:`get_column_types `), the returned instance has ``config`` set to the parsed JSON config dict for that column assignment, or ``None`` if no config was provided. Column types are assigned to columns via the :ref:`column_types ` table configuration option: .. code-block:: yaml databases: mydb: tables: mytable: column_types: bg_color: color highlight: type: color config: format: rgb Datasette includes four built-in column types: ``url``, ``email``, ``json``, and ``textarea``. The ``textarea`` type is an editing hint that causes Datasette's insert/edit forms to use a multiline ``