{"ok": true, "next": null, "rows": [{"id": "plugin_hooks:allow-alice-to-view-a-specific-table", "page": "plugin_hooks", "ref": "allow-alice-to-view-a-specific-table", "title": "Allow Alice to view a specific table", "content": "This plugin grants the actor with  id == \"alice\"  permission to perform the\n                         view-table  action against the  sales  table inside the  accounting  database. \n                     from datasette import hookimpl\nfrom datasette.permissions import PermissionSQL\n\n\n@hookimpl\ndef permission_resources_sql(datasette, actor, action):\n    if action != \"view-table\":\n        return None\n    if not actor or actor.get(\"id\") != \"alice\":\n        return None\n\n    return PermissionSQL(\n        sql=\"\"\"\n            SELECT\n                'accounting' AS parent,\n                'sales' AS child,\n                1 AS allow,\n                'alice can view accounting/sales' AS reason\n        \"\"\",\n    )", "breadcrumbs": "[\"Plugin hooks\", \"permission_resources_sql(datasette, actor, action)\", \"Permission plugin examples\"]", "references": "[]"}, {"id": "plugin_hooks:default-deny-with-an-exception", "page": "plugin_hooks", "ref": "default-deny-with-an-exception", "title": "Default deny with an exception", "content": "Combine a root-level deny with a specific table allow for trusted users.\n                        The resolver will automatically apply the most specific rule. \n                     from datasette import hookimpl\nfrom datasette.permissions import PermissionSQL\n\nTRUSTED = {\"alice\", \"bob\"}\n\n\n@hookimpl\ndef permission_resources_sql(datasette, actor, action):\n    if action != \"view-table\":\n        return None\n\n    actor_id = (actor or {}).get(\"id\")\n\n    if actor_id not in TRUSTED:\n        return PermissionSQL(\n            sql=\"\"\"\n                SELECT NULL AS parent, NULL AS child, 0 AS allow,\n                       'default deny view-table' AS reason\n            \"\"\",\n        )\n\n    return PermissionSQL(\n        sql=\"\"\"\n            SELECT NULL AS parent, NULL AS child, 0 AS allow,\n                   'default deny view-table' AS reason\n            UNION ALL\n            SELECT 'reports' AS parent, 'daily_metrics' AS child, 1 AS allow,\n                   'trusted user access' AS reason\n        \"\"\",\n        params={\"actor_id\": actor_id},\n    ) \n                     The  UNION ALL  ensures the deny rule is always present, while the second row\n                        adds the exception for trusted users.", "breadcrumbs": "[\"Plugin hooks\", \"permission_resources_sql(datasette, actor, action)\", \"Permission plugin examples\"]", "references": "[]"}, {"id": "plugin_hooks:permission-plugin-examples", "page": "plugin_hooks", "ref": "permission-plugin-examples", "title": "Permission plugin examples", "content": "These snippets show how to use the new  permission_resources_sql  hook to\n                    contribute rows to the action-based permission resolver. Each hook receives the\n                    current actor dictionary (or  None ) and must return  None  or an instance or list of\n                     datasette.permissions.PermissionSQL  (or a coroutine that resolves to that).", "breadcrumbs": "[\"Plugin hooks\", \"permission_resources_sql(datasette, actor, action)\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-actions", "page": "plugin_hooks", "ref": "plugin-actions", "title": "Action hooks", "content": "Action hooks can be used to add items to the action menus that appear at the top of different pages within Datasette. Unlike  menu_links() , actions which are displayed on every page, actions should only be relevant to the page the user is currently viewing. \n             Each of these hooks should return return a list of  {\"href\": \"...\", \"label\": \"...\"}  menu items, with optional  \"description\": \"...\"  keys describing each action in more detail. \n             They can alternatively return an  async def  awaitable function which, when called, returns a list of those menu items.", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-event-tracking", "page": "plugin_hooks", "ref": "plugin-event-tracking", "title": "Event tracking", "content": "Datasette includes an internal mechanism for tracking notable events. This can be used for analytics, but can also be used by plugins that want to listen out for when key events occur (such as a table being created) and take action in response. \n             Plugins can register to receive events using the  track_event  plugin hook. \n             They can also define their own events for other plugins to receive using the  register_events() plugin hook , combined with calls to the  datasette.track_event() internal method .", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-hook-forbidden", "page": "plugin_hooks", "ref": "plugin-hook-forbidden", "title": "forbidden(datasette, request, message)", "content": "datasette  -  Datasette class \n                     \n                         You can use this to access plugin configuration options via  datasette.plugin_config(your_plugin_name) , or to render templates or execute SQL queries. \n                     \n                 \n                 \n                     request  -  Request object \n                     \n                         The current HTTP request. \n                     \n                 \n                 \n                     message  - string \n                     \n                         A message hinting at why the request was forbidden. \n                     \n                 \n             \n             Plugins can use this to customize how Datasette responds when a 403 Forbidden error occurs - usually because a page failed a permission check, see  Permissions . \n             If a plugin hook wishes to react to the error, it should return a  Response object . \n             This example returns a redirect to a  /-/login  page: \n             from datasette import hookimpl\nfrom urllib.parse import urlencode\n\n\n@hookimpl\ndef forbidden(request, message):\n    return Response.redirect(\n        \"/-/login?=\" + urlencode({\"message\": message})\n    ) \n             The function can alternatively return an awaitable function if it needs to make any asynchronous method calls. This example renders a template: \n             from datasette import hookimpl, Response\n\n\n@hookimpl\ndef forbidden(datasette):\n    async def inner():\n        return Response.html(\n            await datasette.render_template(\n                \"render_message.html\", request=request\n            )\n        )\n\n    return inner", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-hook-homepage-actions", "page": "plugin_hooks", "ref": "plugin-hook-homepage-actions", "title": "homepage_actions(datasette, actor, request)", "content": "datasette  -  Datasette class \n                         \n                             You can use this to access plugin configuration options via  datasette.plugin_config(your_plugin_name) , or to execute SQL queries. \n                         \n                     \n                     \n                         actor  - dictionary or None \n                         \n                             The currently authenticated  actor . \n                         \n                     \n                     \n                         request  -  Request object \n                         \n                             The current HTTP request. \n                         \n                     \n                 \n                 Populates an actions menu on the top-level index homepage of the Datasette instance. \n                 This example adds a link an imagined tool for editing the homepage, only for signed in users: \n                 from datasette import hookimpl\n\n\n@hookimpl\ndef homepage_actions(datasette, actor):\n    if actor:\n        return [\n            {\n                \"href\": datasette.urls.path(\n                    \"/-/customize-homepage\"\n                ),\n                \"label\": \"Customize homepage\",\n            }\n        ]", "breadcrumbs": "[\"Plugin hooks\", \"Action hooks\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-hook-permission-resources-sql", "page": "plugin_hooks", "ref": "plugin-hook-permission-resources-sql", "title": "permission_resources_sql(datasette, actor, action)", "content": "datasette  -  Datasette class \n                     \n                         Access to the Datasette instance. \n                     \n                 \n                 \n                     actor  - dictionary or None \n                     \n                         The current actor dictionary.  None  for anonymous requests. \n                     \n                 \n                 \n                     action  - string \n                     \n                         The permission action being evaluated. Examples include  \"view-table\"  or  \"insert-row\" . \n                     \n                 \n                 \n                     Return value \n                     \n                         A  datasette.permissions.PermissionSQL  object,  None  or an iterable of  PermissionSQL  objects. \n                     \n                 \n             \n             Datasette's action-based permission resolver calls this hook to gather SQL rows describing which\n                resources an actor may access ( allow = 1 ) or should be denied ( allow = 0 ) for a specific action.\n                Each SQL snippet should return  parent ,  child ,  allow  and  reason  columns. \n             Parameter naming convention:  Plugin parameters in  PermissionSQL.params  should use unique names\n                to avoid conflicts with other plugins. The recommended convention is to prefix parameters with your\n                plugin's source name (e.g.,  myplugin_user_id ). The system reserves these parameter names:\n                 :actor ,  :actor_id ,  :action , and  :filter_parent . \n             You can also use return  PermissionSQL.allow(reason=\"reason goes here\")  or  PermissionSQL.deny(reason=\"reason goes here\")  as shortcuts for simple root-level allow or deny rules. These will create SQL snippets that look like this: \n             SELECT\n    NULL AS parent,\n    NULL AS child,\n    1 AS allow,\n    'reason goes here' AS reason \n             Or  0 AS allow  for denies.", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-hook-register-events", "page": "plugin_hooks", "ref": "plugin-hook-register-events", "title": "register_events(datasette)", "content": "datasette  -  Datasette class \n                         \n                             You can use this to access plugin configuration options via  datasette.plugin_config(your_plugin_name) . \n                         \n                     \n                 \n                 This hook should return a list of  Event  subclasses that represent custom events that the plugin might send to the  datasette.track_event()  method. \n                 This example registers event subclasses for  ban-user  and  unban-user  events: \n                 from dataclasses import dataclass\nfrom datasette import hookimpl, Event\n\n\n@dataclass\nclass BanUserEvent(Event):\n    name = \"ban-user\"\n    user: dict\n\n\n@dataclass\nclass UnbanUserEvent(Event):\n    name = \"unban-user\"\n    user: dict\n\n\n@hookimpl\ndef register_events():\n    return [BanUserEvent, UnbanUserEvent] \n                 The plugin can then call  datasette.track_event(...)  to send a  ban-user  event: \n                 await datasette.track_event(\n    BanUserEvent(user={\"id\": 1, \"username\": \"cleverbot\"})\n)", "breadcrumbs": "[\"Plugin hooks\", \"Event tracking\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-hook-register-magic-parameters", "page": "plugin_hooks", "ref": "plugin-hook-register-magic-parameters", "title": "register_magic_parameters(datasette)", "content": "datasette  -  Datasette class \n                     \n                         You can use this to access plugin configuration options via  datasette.plugin_config(your_plugin_name) . \n                     \n                 \n             \n             Magic parameters  can be used to add automatic parameters to  canned queries . This plugin hook allows additional magic parameters to be defined by plugins. \n             Magic parameters all take this format:  _prefix_rest_of_parameter . The prefix indicates which magic parameter function should be called - the rest of the parameter is passed as an argument to that function. \n             To register a new function, return it as a tuple of  (string prefix, function)  from this hook. The function you register should take two arguments:  key  and  request , where  key  is the  rest_of_parameter  portion of the parameter and  request  is the current  Request object . \n             This example registers two new magic parameters:  :_request_http_version  returning the HTTP version of the current request, and  :_uuid_new  which returns a new UUID. It also registers an  :_asynclookup_key  parameter, demonstrating that these functions can be asynchronous: \n             from datasette import hookimpl\nfrom uuid import uuid4\n\n\ndef uuid(key, request):\n    if key == \"new\":\n        return str(uuid4())\n    else:\n        raise KeyError\n\n\ndef request(key, request):\n    if key == \"http_version\":\n        return request.scope[\"http_version\"]\n    else:\n        raise KeyError\n\n\nasync def asynclookup(key, request):\n    return await do_something_async(key)\n\n\n@hookimpl\ndef register_magic_parameters(datasette):\n    return [\n        (\"request\", request),\n        (\"uuid\", uuid),\n        (\"asynclookup\", asynclookup),\n    ]", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-hook-top-canned-query", "page": "plugin_hooks", "ref": "plugin-hook-top-canned-query", "title": "top_canned_query(datasette, request, database, query_name)", "content": "datasette  -  Datasette class \n                         \n                             You can use this to access plugin configuration options via  datasette.plugin_config(your_plugin_name) . \n                         \n                     \n                     \n                         request  -  Request object \n                         \n                             The current HTTP request. \n                         \n                     \n                     \n                         database  - string \n                         \n                             The name of the database. \n                         \n                     \n                     \n                         query_name  - string \n                         \n                             The name of the canned query. \n                         \n                     \n                 \n                 Returns HTML to be displayed at the top of the canned query page.", "breadcrumbs": "[\"Plugin hooks\", \"Template slots\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-hook-top-database", "page": "plugin_hooks", "ref": "plugin-hook-top-database", "title": "top_database(datasette, request, database)", "content": "datasette  -  Datasette class \n                         \n                             You can use this to access plugin configuration options via  datasette.plugin_config(your_plugin_name) . \n                         \n                     \n                     \n                         request  -  Request object \n                         \n                             The current HTTP request. \n                         \n                     \n                     \n                         database  - string \n                         \n                             The name of the database. \n                         \n                     \n                 \n                 Returns HTML to be displayed at the top of the database page.", "breadcrumbs": "[\"Plugin hooks\", \"Template slots\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-hook-top-homepage", "page": "plugin_hooks", "ref": "plugin-hook-top-homepage", "title": "top_homepage(datasette, request)", "content": "datasette  -  Datasette class \n                         \n                             You can use this to access plugin configuration options via  datasette.plugin_config(your_plugin_name) . \n                         \n                     \n                     \n                         request  -  Request object \n                         \n                             The current HTTP request. \n                         \n                     \n                 \n                 Returns HTML to be displayed at the top of the Datasette homepage.", "breadcrumbs": "[\"Plugin hooks\", \"Template slots\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-hook-top-query", "page": "plugin_hooks", "ref": "plugin-hook-top-query", "title": "top_query(datasette, request, database, sql)", "content": "datasette  -  Datasette class \n                         \n                             You can use this to access plugin configuration options via  datasette.plugin_config(your_plugin_name) . \n                         \n                     \n                     \n                         request  -  Request object \n                         \n                             The current HTTP request. \n                         \n                     \n                     \n                         database  - string \n                         \n                             The name of the database. \n                         \n                     \n                     \n                         sql  - string \n                         \n                             The SQL query. \n                         \n                     \n                 \n                 Returns HTML to be displayed at the top of the query results page.", "breadcrumbs": "[\"Plugin hooks\", \"Template slots\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-hook-top-row", "page": "plugin_hooks", "ref": "plugin-hook-top-row", "title": "top_row(datasette, request, database, table, row)", "content": "datasette  -  Datasette class \n                         \n                             You can use this to access plugin configuration options via  datasette.plugin_config(your_plugin_name) . \n                         \n                     \n                     \n                         request  -  Request object \n                         \n                             The current HTTP request. \n                         \n                     \n                     \n                         database  - string \n                         \n                             The name of the database. \n                         \n                     \n                     \n                         table  - string \n                         \n                             The name of the table. \n                         \n                     \n                     \n                         row  -  sqlite.Row \n                         \n                             The SQLite row object being displayed. \n                         \n                     \n                 \n                 Returns HTML to be displayed at the top of the row page.", "breadcrumbs": "[\"Plugin hooks\", \"Template slots\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-hook-top-table", "page": "plugin_hooks", "ref": "plugin-hook-top-table", "title": "top_table(datasette, request, database, table)", "content": "datasette  -  Datasette class \n                         \n                             You can use this to access plugin configuration options via  datasette.plugin_config(your_plugin_name) . \n                         \n                     \n                     \n                         request  -  Request object \n                         \n                             The current HTTP request. \n                         \n                     \n                     \n                         database  - string \n                         \n                             The name of the database. \n                         \n                     \n                     \n                         table  - string \n                         \n                             The name of the table. \n                         \n                     \n                 \n                 Returns HTML to be displayed at the top of the table page.", "breadcrumbs": "[\"Plugin hooks\", \"Template slots\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-hook-view-actions", "page": "plugin_hooks", "ref": "plugin-hook-view-actions", "title": "view_actions(datasette, actor, database, view, request)", "content": "datasette  -  Datasette class \n                         \n                             You can use this to access plugin configuration options via  datasette.plugin_config(your_plugin_name) , or to execute SQL queries. \n                         \n                     \n                     \n                         actor  - dictionary or None \n                         \n                             The currently authenticated  actor . \n                         \n                     \n                     \n                         database  - string \n                         \n                             The name of the database. \n                         \n                     \n                     \n                         view  - string \n                         \n                             The name of the SQL view. \n                         \n                     \n                     \n                         request  -  Request object  or None \n                         \n                             The current HTTP request. This can be  None  if the request object is not available. \n                         \n                     \n                 \n                 Like  table_actions(datasette, actor, database, table, request)  but for SQL views.", "breadcrumbs": "[\"Plugin hooks\", \"Action hooks\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-hook-write-wrapper", "page": "plugin_hooks", "ref": "plugin-hook-write-wrapper", "title": "write_wrapper(datasette, database, request, transaction)", "content": "datasette  -  Datasette class \n                     \n                         You can use this to access plugin configuration options via  datasette.plugin_config(your_plugin_name) . \n                     \n                 \n                 \n                     database  - string \n                     \n                         The name of the database being written to. \n                     \n                 \n                 \n                     request  -  Request object  or  None \n                     \n                         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). \n                     \n                 \n                 \n                     transaction  - bool \n                     \n                         True  if the write will be wrapped in a database transaction. \n                     \n                 \n             \n             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. \n             The result of the write function is sent back through the  yield , so you can capture it with  result = yield . \n             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 . \n             If your generator accepts  track_event , you can call  track_event(event)  to queue an event that will be dispatched via  datasette.track_event()  after the write commits successfully.  Events are discarded if the write raises an exception. \n             Return  None  to skip wrapping for this particular write. \n             This example logs every write operation: \n             from datasette import hookimpl\n\n\n@hookimpl\ndef write_wrapper(datasette, database, request):\n    def wrapper(conn):\n        print(f\"Before write to {database}\")\n        result = yield\n        print(f\"After write to {database}\")\n\n    return wrapper \n             This more advanced example uses the SQLite authorizer callback to block writes to a specific table for non-admin users: \n             import sqlite3\nfrom datasette import hookimpl\n\nWRITE_ACTIONS = (\n    sqlite3.SQLITE_INSERT,\n    sqlite3.SQLITE_UPDATE,\n    sqlite3.SQLITE_DELETE,\n)\n\n\n@hookimpl\ndef write_wrapper(datasette, database, request):\n    actor = None\n    if request:\n        actor = request.actor\n    if actor and actor.get(\"id\") == \"admin\":\n        return None\n\n    def wrapper(conn):\n        def authorizer(\n            action, arg1, arg2, db_name, trigger\n        ):\n            if (\n                action in WRITE_ACTIONS\n                and arg1 == \"protected_table\"\n            ):\n                return sqlite3.SQLITE_DENY\n            return sqlite3.SQLITE_OK\n\n        conn.set_authorizer(authorizer)\n        try:\n            yield\n        finally:\n            conn.set_authorizer(None)\n\n    return wrapper \n             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. \n             When multiple plugins implement  write_wrapper , they are nested following pluggy's default calling convention.", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-page-extras", "page": "plugin_hooks", "ref": "plugin-page-extras", "title": "Page extras", "content": "These plugin hooks can be used to affect the way HTML pages for different Datasette interfaces are rendered.", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-register-actions", "page": "plugin_hooks", "ref": "plugin-register-actions", "title": "register_actions(datasette)", "content": "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. \n             Actions define what operations can be performed on resources (like viewing a table, executing SQL, or custom plugin actions). \n             from datasette import hookimpl\nfrom datasette.permissions import Action, Resource\n\n\nclass DocumentCollectionResource(Resource):\n    \"\"\"A collection of documents.\"\"\"\n\n    name = \"document-collection\"\n    parent_class = None\n\n    def __init__(self, collection: str):\n        super().__init__(parent=collection, child=None)\n\n    @classmethod\n    async def resources_sql(\n        cls, datasette, actor=None\n    ) -> str:\n        return \"\"\"\n            SELECT collection_name AS parent, NULL AS child\n            FROM document_collections\n        \"\"\"\n\n\nclass DocumentResource(Resource):\n    \"\"\"A document in a collection.\"\"\"\n\n    name = \"document\"\n    parent_class = DocumentCollectionResource\n\n    def __init__(self, collection: str, document: str):\n        super().__init__(parent=collection, child=document)\n\n    @classmethod\n    async def resources_sql(\n        cls, datasette, actor=None\n    ) -> str:\n        return \"\"\"\n            SELECT collection_name AS parent, document_id AS child\n            FROM documents\n        \"\"\"\n\n\n@hookimpl\ndef register_actions(datasette):\n    return [\n        Action(\n            name=\"list-documents\",\n            abbr=\"ld\",\n            description=\"List documents in a collection\",\n            resource_class=DocumentCollectionResource,\n        ),\n        Action(\n            name=\"view-document\",\n            abbr=\"vdoc\",\n            description=\"View document\",\n            resource_class=DocumentResource,\n        ),\n        Action(\n            name=\"edit-document\",\n            abbr=\"edoc\",\n            description=\"Edit document\",\n            resource_class=DocumentResource,\n        ),\n    ] \n             The fields of the  Action  dataclass are as follows: \n             \n                 \n                     name  - string \n                     \n                         The name of the action, e.g.  view-document . This should be unique across all plugins. \n                     \n                 \n                 \n                     abbr  - string or None \n                     \n                         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 .) \n                     \n                 \n                 \n                     description  - string or None \n                     \n                         A human-readable description of what the action allows you to do. \n                     \n                 \n                 \n                     resource_class  - type[Resource] or None \n                     \n                         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: \n                         \n                             \n                                 Define a  name  class attribute (e.g.,  \"document\" ) \n                             \n                             \n                                 Define a  parent_class  class attribute ( None  for top-level resources like databases, or the parent  Resource  subclass for child resources) \n                             \n                             \n                                 Implement an async  resources_sql(cls, datasette, actor=None)  classmethod that returns SQL returning all resources as  (parent, child)  columns \n                             \n                             \n                                 Have an  __init__  method that accepts appropriate parameters and calls  super().__init__(parent=..., child=...)", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-register-column-types", "page": "plugin_hooks", "ref": "plugin-register-column-types", "title": "register_column_types(datasette)", "content": "Return a list of  ColumnType   subclasses  (not instances) to register custom column types. Column types define how values in specific columns are rendered, validated, and transformed. \n             from datasette import hookimpl\nfrom datasette.column_types import ColumnType, SQLiteType\nimport markupsafe\n\n\nclass ColorColumnType(ColumnType):\n    name = \"color\"\n    description = \"CSS color value\"\n    sqlite_types = (SQLiteType.TEXT,)\n\n    async def render_cell(\n        self,\n        value,\n        column,\n        table,\n        database,\n        datasette,\n        request,\n    ):\n        if value:\n            return markupsafe.Markup(\n                '<span style=\"background-color: {color}\">'\n                \"{color}</span>\"\n            ).format(color=markupsafe.escape(value))\n        return None\n\n    async def validate(self, value, datasette):\n        if value and not value.startswith(\"#\"):\n            return \"Color must start with #\"\n        return None\n\n    async def transform_value(self, value, datasette):\n        # Normalize to uppercase\n        if isinstance(value, str):\n            return value.upper()\n        return value\n\n\n@hookimpl\ndef register_column_types(datasette):\n    return [ColorColumnType] \n             Each  ColumnType  subclass must define the following class attributes: \n             \n                 \n                     name  - string \n                     \n                         Unique identifier for the column type, e.g.  \"color\" . Must be unique across all plugins. \n                     \n                 \n                 \n                     description  - string \n                     \n                         Human-readable label, e.g.  \"CSS color value\" . \n                     \n                 \n                 \n                     sqlite_types  - tuple of  SQLiteType  values, optional \n                     \n                         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. \n                     \n                 \n             \n             And the following methods, all optional: \n             \n                 \n                     render_cell(self, value, column, table, database, datasette, request) \n                     \n                         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. \n                     \n                 \n                 \n                     validate(self, value, datasette) \n                     \n                         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. \n                     \n                 \n                 \n                     transform_value(self, value, datasette) \n                     \n                         Transform a value before it appears in JSON API output. Return the transformed value. The default implementation returns the value unchanged. \n                     \n                 \n             \n             Per-column configuration is available via  self.config  in all methods. When a column type is looked up for a specific column (via  get_column_type  or  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. \n             Column types are assigned to columns via the  column_types  table configuration option: \n             databases:\n  mydb:\n    tables:\n      mytable:\n        column_types:\n          bg_color: color\n          highlight:\n            type: color\n            config:\n              format: rgb \n             Datasette includes three built-in column types:  url ,  email , and  json .", "breadcrumbs": "[\"Plugin hooks\"]", "references": "[]"}, {"id": "plugin_hooks:plugin-resources-sql", "page": "plugin_hooks", "ref": "plugin-resources-sql", "title": "The ", "content": "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. \n                 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: \n                 \n                     \n                         Get all resources of this type from your data catalog \n                     \n                     \n                         Combine it with permission rules from the  permission_resources_sql  hook \n                     \n                     \n                         Use SQL joins and filtering to determine which resources the actor can access \n                     \n                     \n                         Return only the permitted resources \n                     \n                 \n                 The SQL query  must  return exactly two columns: \n                 \n                     \n                         parent  - The parent identifier (e.g., database name, collection name), or  NULL  for top-level resources \n                     \n                     \n                         child  - The child identifier (e.g., table name, document ID), or  NULL  for parent-only resources \n                     \n                 \n                 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: \n                 @classmethod\nasync def resources_sql(cls, datasette, actor=None) -> str:\n    return \"\"\"\n        SELECT collection_name AS parent, document_id AS child\n        FROM documents\n    \"\"\" \n                 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.\" \n                 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.", "breadcrumbs": "[\"Plugin hooks\", \"register_actions(datasette)\"]", "references": "[]"}, {"id": "plugin_hooks:read-permissions-from-a-custom-table", "page": "plugin_hooks", "ref": "read-permissions-from-a-custom-table", "title": "Read permissions from a custom table", "content": "This example stores grants in an internal table called  permission_grants \n                        with columns  (actor_id, action, parent, child, allow, reason) . \n                     from datasette import hookimpl\nfrom datasette.permissions import PermissionSQL\n\n\n@hookimpl\ndef permission_resources_sql(datasette, actor, action):\n    if not actor:\n        return None\n\n    return PermissionSQL(\n        sql=\"\"\"\n            SELECT\n                parent,\n                child,\n                allow,\n                COALESCE(reason, 'permission_grants table') AS reason\n            FROM permission_grants\n            WHERE actor_id = :grants_actor_id\n              AND action = :grants_action\n        \"\"\",\n        params={\n            \"grants_actor_id\": actor.get(\"id\"),\n            \"grants_action\": action,\n        },\n    )", "breadcrumbs": "[\"Plugin hooks\", \"permission_resources_sql(datasette, actor, action)\", \"Permission plugin examples\"]", "references": "[]"}, {"id": "plugin_hooks:restrict-execute-sql-to-a-database-prefix", "page": "plugin_hooks", "ref": "restrict-execute-sql-to-a-database-prefix", "title": "Restrict execute-sql to a database prefix", "content": "Only allow  execute-sql  against databases whose name begins with\n                         analytics_ . This shows how to use parameters that the permission resolver\n                        will pass through to the SQL snippet. \n                     from datasette import hookimpl\nfrom datasette.permissions import PermissionSQL\n\n\n@hookimpl\ndef permission_resources_sql(datasette, actor, action):\n    if action != \"execute-sql\":\n        return None\n\n    return PermissionSQL(\n        sql=\"\"\"\n            SELECT\n                parent,\n                NULL AS child,\n                1 AS allow,\n                'execute-sql allowed for analytics_*' AS reason\n            FROM catalog_databases\n            WHERE database_name LIKE :analytics_prefix\n        \"\"\",\n        params={\n            \"analytics_prefix\": \"analytics_%\",\n        },\n    )", "breadcrumbs": "[\"Plugin hooks\", \"permission_resources_sql(datasette, actor, action)\", \"Permission plugin examples\"]", "references": "[]"}], "truncated": false}