rowid,title,content,sections_fts,rank
1,Events,"Datasette includes a mechanism for tracking events that occur while the software is running. This is primarily intended to be used by plugins, which can both trigger events and listen for events.
The core Datasette application triggers events when certain things happen. This page describes those events.
Plugins can listen for events using the track_event(datasette, event) plugin hook, which will be called with instances of the following classes - or additional classes registered by other plugins .
class datasette.events. LoginEvent actor : dict | None
Event name: login
A user (represented by event.actor ) has logged in.
class datasette.events. LogoutEvent actor : dict | None
Event name: logout
A user (represented by event.actor ) has logged out.
class datasette.events. CreateTokenEvent actor : dict | None expires_after : int | None restrict_all : list restrict_database : dict restrict_resource : dict
Event name: create-token
A user created an API token.
Variables
expires_after -- Number of seconds after which this token will expire.
restrict_all -- Restricted permissions for this token.
restrict_database -- Restricted database permissions for this token.
restrict_resource -- Restricted resource permissions for this token.
class datasette.events. CreateTableEvent actor : dict | None database : str table : str schema : str
Event name: create-table
A new table has been created in the database.
Variables
database -- The name of the database where the table was created.
table -- The name of the table that was created
schema -- The SQL schema definition for the new table.
class datasette.events. DropTableEvent actor : dict | None database : str table : str
Event name: drop-table
A table has been dropped from the database.
Variables
database -- The name of the database where the table was dropped.
table -- The name of the table that was dropped
class datasette.events. AlterTableEvent actor : dict | None database : str table : str before_schema : str after_schema : str
Event name: alter-table
A table has been altered.
Variables
database -- The name of the database where the table was altered
table -- The name of the table that was altered
before_schema -- The table's SQL schema before the alteration
after_schema -- The table's SQL schema after the alteration
class datasette.events. InsertRowsEvent actor : dict | None database : str table : str num_rows : int ignore : bool replace : bool
Event name: insert-rows
Rows were inserted into a table.
Variables
database -- The name of the database where the rows were inserted.
table -- The name of the table where the rows were inserted.
num_rows -- The number of rows that were requested to be inserted.
ignore -- Was ignore set?
replace -- Was replace set?
class datasette.events. UpsertRowsEvent actor : dict | None database : str table : str num_rows : int
Event name: upsert-rows
Rows were upserted into a table.
Variables
database -- The name of the database where the rows were inserted.
table -- The name of the table where the rows were inserted.
num_rows -- The number of rows that were requested to be inserted.
class datasette.events. UpdateRowEvent actor : dict | None database : str table : str pks : list
Event name: update-row
A row was updated in a table.
Variables
database -- The name of the database where the row was updated.
table -- The name of the table where the row was updated.
pks -- The primary key values of the updated row.
class datasette.events. DeleteRowEvent actor : dict | None database : str table : str pks : list
Event name: delete-row
A row was deleted from a table.
Variables
database -- The name of the database where the row was deleted.
table -- The name of the table where the row was deleted.
pks -- The primary key values of the deleted row.",14,
2,Facets,"Datasette facets can be used to add a faceted browse interface to any database table.
With facets, tables are displayed along with a summary showing the most common values in specified columns.
These values can be selected to further filter the table.
Here's an example :
Facets can be specified in two ways: using query string parameters, or in metadata.json configuration for the table.",14,
3,Facets in query strings,"To turn on faceting for specific columns on a Datasette table view, add one or more _facet=COLUMN parameters to the URL.
For example, if you want to turn on facets for the city_id and state columns, construct a URL that looks like this:
/dbname/tablename?_facet=state&_facet=city_id
This works for both the HTML interface and the .json view.
When enabled, facets will cause a facet_results block to be added to the JSON output, looking something like this:
{
""state"": {
""name"": ""state"",
""results"": [
{
""value"": ""CA"",
""label"": ""CA"",
""count"": 10,
""toggle_url"": ""http://...?_facet=city_id&_facet=state&state=CA"",
""selected"": false
},
{
""value"": ""MI"",
""label"": ""MI"",
""count"": 4,
""toggle_url"": ""http://...?_facet=city_id&_facet=state&state=MI"",
""selected"": false
},
{
""value"": ""MC"",
""label"": ""MC"",
""count"": 1,
""toggle_url"": ""http://...?_facet=city_id&_facet=state&state=MC"",
""selected"": false
}
],
""truncated"": false
}
""city_id"": {
""name"": ""city_id"",
""results"": [
{
""value"": 1,
""label"": ""San Francisco"",
""count"": 6,
""toggle_url"": ""http://...?_facet=city_id&_facet=state&city_id=1"",
""selected"": false
},
{
""value"": 2,
""label"": ""Los Angeles"",
""count"": 4,
""toggle_url"": ""http://...?_facet=city_id&_facet=state&city_id=2"",
""selected"": false
},
{
""value"": 3,
""label"": ""Detroit"",
""count"": 4,
""toggle_url"": ""http://...?_facet=city_id&_facet=state&city_id=3"",
""selected"": false
},
{
""value"": 4,
""label"": ""Memnonia"",
""count"": 1,
""toggle_url"": ""http://...?_facet=city_id&_facet=state&city_id=4"",
""selected"": false
}
],
""truncated"": false
}
}
If Datasette detects that a column is a foreign key, the ""label"" property will be automatically derived from the detected label column on the referenced table.
The default number of facet results returned is 30, controlled by the default_facet_size setting.
You can increase this on an individual page by adding ?_facet_size=100 to the query string, up to a maximum of max_returned_rows (which defaults to 1000).",14,
4,Facets in metadata,"You can turn facets on by default for specific tables by adding them to a ""facets"" key in a Datasette Metadata file.
Here's an example that turns on faceting by default for the qLegalStatus column in the Street_Tree_List table in the sf-trees database:
[[[cog
from metadata_doc import metadata_example
metadata_example(cog, {
""databases"": {
""sf-trees"": {
""tables"": {
""Street_Tree_List"": {
""facets"": [""qLegalStatus""]
}
}
}
}
})
]]]
[[[end]]]
Facets defined in this way will always be shown in the interface and returned in the API, regardless of the _facet arguments passed to the view.
You can specify array or date facets in metadata using JSON objects with a single key of array or date and a value specifying the column, like this:
[[[cog
metadata_example(cog, {
""facets"": [
{""array"": ""tags""},
{""date"": ""created""}
]
})
]]]
[[[end]]]
You can change the default facet size (the number of results shown for each facet) for a table using facet_size :
[[[cog
metadata_example(cog, {
""databases"": {
""sf-trees"": {
""tables"": {
""Street_Tree_List"": {
""facets"": [""qLegalStatus""],
""facet_size"": 10
}
}
}
}
})
]]]
[[[end]]]",14,
5,Suggested facets,"Datasette's table UI will suggest facets for the user to apply, based on the following criteria:
For the currently filtered data are there any columns which, if applied as a facet...
Will return 30 or less unique options
Will return more than one unique option
Will return less unique options than the total number of filtered rows
And the query used to evaluate this criteria can be completed in under 50ms
That last point is particularly important: Datasette runs a query for every column that is displayed on a page, which could get expensive - so to avoid slow load times it sets a time limit of just 50ms for each of those queries.
This means suggested facets are unlikely to appear for tables with millions of records in them.",14,
6,Speeding up facets with indexes,"The performance of facets can be greatly improved by adding indexes on the columns you wish to facet by.
Adding indexes can be performed using the sqlite3 command-line utility. Here's how to add an index on the state column in a table called Food_Trucks :
sqlite3 mydatabase.db
SQLite version 3.19.3 2017-06-27 16:48:08
Enter "".help"" for usage hints.
sqlite> CREATE INDEX Food_Trucks_state ON Food_Trucks(""state"");
Or using the sqlite-utils command-line utility:
sqlite-utils create-index mydatabase.db Food_Trucks state",14,
7,Facet by JSON array,"If your SQLite installation provides the json1 extension (you can check using /-/versions ) Datasette will automatically detect columns that contain JSON arrays of values and offer a faceting interface against those columns.
This is useful for modelling things like tags without needing to break them out into a new table.
Example here: latest.datasette.io/fixtures/facetable?_facet_array=tags",14,
8,Facet by date,"If Datasette finds any columns that contain dates in the first 100 values, it will offer a faceting interface against the dates of those values.
This works especially well against timestamp values such as 2019-03-01 12:44:00 .
Example here: latest.datasette.io/fixtures/facetable?_facet_date=created",14,
9,Full-text search,"SQLite includes a powerful mechanism for enabling full-text search against SQLite records. Datasette can detect if a table has had full-text search configured for it in the underlying database and display a search interface for filtering that table.
Here's an example search :
Datasette automatically detects which tables have been configured for full-text search.",14,
10,The table page and table view API,"Table views that support full-text search can be queried using the ?_search=TERMS query string parameter. This will run the search against content from all of the columns that have been included in the index.
Try this example: fara.datasettes.com/fara/FARA_All_ShortForms?_search=manafort
SQLite full-text search supports wildcards. This means you can easily implement prefix auto-complete by including an asterisk at the end of the search term - for example:
/dbname/tablename/?_search=rob*
This will return all records containing at least one word that starts with the letters rob .
You can also run searches against just the content of a specific named column by using _search_COLNAME=TERMS - for example, this would search for just rows where the name column in the FTS index mentions Sarah :
/dbname/tablename/?_search_name=Sarah",14,
11,Advanced SQLite search queries,"SQLite full-text search includes support for a variety of advanced queries , including AND , OR , NOT and NEAR .
By default Datasette disables these features to ensure they do not cause errors or confusion for users who are not aware of them. You can disable this escaping and use the advanced queries by adding &_searchmode=raw to the table page query string.
If you want to enable these operators by default for a specific table, you can do so by adding ""searchmode"": ""raw"" to the metadata configuration for that table, see Configuring full-text search for a table or view .
If that option has been specified in the table metadata but you want to over-ride it and return to the default behavior you can append &_searchmode=escaped to the query string.",14,
12,Configuring full-text search for a table or view,"If a table has a corresponding FTS table set up using the content= argument to CREATE VIRTUAL TABLE shown below, Datasette will detect it automatically and add a search interface to the table page for that table.
You can also manually configure which table should be used for full-text search using query string parameters or Metadata . You can set the associated FTS table for a specific table and you can also set one for a view - if you do that, the page for that SQL view will offer a search option.
Use ?_fts_table=x to over-ride the FTS table for a specific page. If the primary key was something other than rowid you can use ?_fts_pk=col to set that as well. This is particularly useful for views, for example:
https://latest.datasette.io/fixtures/searchable_view?_fts_table=searchable_fts&_fts_pk=pk
The fts_table metadata property can be used to specify an associated FTS table. If the primary key column in your table which was used to populate the FTS table is something other than rowid , you can specify the column to use with the fts_pk property.
The ""searchmode"": ""raw"" property can be used to default the table to accepting SQLite advanced search operators, as described in Advanced SQLite search queries .
Here is an example which enables full-text search (with SQLite advanced search operators) for a display_ads view which is defined against the ads table and hence needs to run FTS against the ads_fts table, using the id as the primary key:
[[[cog
from metadata_doc import metadata_example
metadata_example(cog, {
""databases"": {
""russian-ads"": {
""tables"": {
""display_ads"": {
""fts_table"": ""ads_fts"",
""fts_pk"": ""id"",
""searchmode"": ""raw""
}
}
}
}
})
]]]
[[[end]]]",14,
13,Searches using custom SQL,"You can include full-text search results in custom SQL queries. The general pattern with SQLite search is to run the search as a sub-select that returns rowid values, then include those rowids in another part of the query.
You can see the syntax for a basic search by running that search on a table page and then clicking ""View and edit SQL"" to see the underlying SQL. For example, consider this search for manafort is the US FARA database :
/fara/FARA_All_ShortForms?_search=manafort
If you click View and edit SQL you'll see that the underlying SQL looks like this:
select
rowid,
Short_Form_Termination_Date,
Short_Form_Date,
Short_Form_Last_Name,
Short_Form_First_Name,
Registration_Number,
Registration_Date,
Registrant_Name,
Address_1,
Address_2,
City,
State,
Zip
from
FARA_All_ShortForms
where
rowid in (
select
rowid
from
FARA_All_ShortForms_fts
where
FARA_All_ShortForms_fts match escape_fts(:search)
)
order by
rowid
limit
101",14,
14,Enabling full-text search for a SQLite table,"Datasette takes advantage of the external content mechanism in SQLite, which allows a full-text search virtual table to be associated with the contents of another SQLite table.
To set up full-text search for a table, you need to do two things:
Create a new FTS virtual table associated with your table
Populate that FTS table with the data that you would like to be able to run searches against",14,
15,Configuring FTS using sqlite-utils,"sqlite-utils is a CLI utility and Python library for manipulating SQLite databases. You can use it from Python code to configure FTS search, or you can achieve the same goal using the accompanying command-line tool .
Here's how to use sqlite-utils to enable full-text search for an items table across the name and description columns:
sqlite-utils enable-fts mydatabase.db items name description",14,
16,Configuring FTS using csvs-to-sqlite,"If your data starts out in CSV files, you can use Datasette's companion tool csvs-to-sqlite to convert that file into a SQLite database and enable full-text search on specific columns. For a file called items.csv where you want full-text search to operate against the name and description columns you would run the following:
csvs-to-sqlite items.csv items.db -f name -f description",14,
17,Configuring FTS by hand,"We recommend using sqlite-utils , but if you want to hand-roll a SQLite full-text search table you can do so using the following SQL.
To enable full-text search for a table called items that works against the name and description columns, you would run this SQL to create a new items_fts FTS virtual table:
CREATE VIRTUAL TABLE ""items_fts"" USING FTS4 (
name,
description,
content=""items""
);
This creates a set of tables to power full-text search against items . The new items_fts table will be detected by Datasette as the fts_table for the items table.
Creating the table is not enough: you also need to populate it with a copy of the data that you wish to make searchable. You can do that using the following SQL:
INSERT INTO ""items_fts"" (rowid, name, description)
SELECT rowid, name, description FROM items;
If your table has columns that are foreign key references to other tables you can include that data in your full-text search index using a join. Imagine the items table has a foreign key column called category_id which refers to a categories table - you could create a full-text search table like this:
CREATE VIRTUAL TABLE ""items_fts"" USING FTS4 (
name,
description,
category_name,
content=""items""
);
And then populate it like this:
INSERT INTO ""items_fts"" (rowid, name, description, category_name)
SELECT items.rowid,
items.name,
items.description,
categories.name
FROM items JOIN categories ON items.category_id=categories.id;
You can use this technique to populate the full-text search index from any combination of tables and joins that makes sense for your project.",14,
18,FTS versions,"There are three different versions of the SQLite FTS module: FTS3, FTS4 and FTS5. You can tell which versions are supported by your instance of Datasette by checking the /-/versions page.
FTS5 is the most advanced module but may not be available in the SQLite version that is bundled with your Python installation. Most importantly, FTS5 is the only version that has the ability to order by search relevance without needing extra code.
If you can't be sure that FTS5 will be available, you should use FTS4.",14,
19,Plugin hooks,"Datasette 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, database, datasette) :
@hookimpl
def render_cell(value, column):
if column == ""stars"":
return ""*"" * int(value)
List of plugin hooks
prepare_connection(conn, database, datasette)
prepare_jinja2_environment(env, datasette)
Page extras
extra_template_vars(template, database, table, columns, view_name, request, datasette)
extra_css_urls(template, database, table, columns, view_name, request, datasette)
extra_js_urls(template, database, table, columns, view_name, request, datasette)
extra_body_script(template, database, table, columns, view_name, request, datasette)
publish_subcommand(publish)
render_cell(row, value, column, table, database, datasette, request)
register_output_renderer(datasette)
register_routes(datasette)
register_commands(cli)
register_facet_classes()
register_permissions(datasette)
asgi_wrapper(datasette)
startup(datasette)
canned_queries(datasette, database, actor)
actor_from_request(datasette, request)
actors_from_ids(datasette, actor_ids)
jinja2_environment_from_request(datasette, request, env)
filters_from_request(request, database, table, datasette)
permission_allowed(datasette, actor, action, resource)
register_magic_parameters(datasette)
forbidden(datasette, request, message)
handle_exception(datasette, request, exception)
skip_csrf(datasette, scope)
get_metadata(datasette, key, database, table)
menu_links(datasette, actor, request)
Action hooks
table_actions(datasette, actor, database, table, request)
view_actions(datasette, actor, database, view, request)
query_actions(datasette, actor, database, query_name, request, sql, params)
row_actions(datasette, actor, request, database, table, row)
database_actions(datasette, actor, database, request)
homepage_actions(datasette, actor, request)
Template slots
top_homepage(datasette, request)
top_database(datasette, request, database)
top_table(datasette, request, database, table)
top_row(datasette, request, database, table, row)
top_query(datasette, request, database, sql)
top_canned_query(datasette, request, database, query_name)
Event tracking
track_event(datasette, event)
register_events(datasette)",14,
20,"prepare_connection(conn, database, datasette)","conn - sqlite3 connection object
The connection that is being opened
database - string
The name of the database
datasette - Datasette class
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:
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);
Examples: datasette-jellyfish , datasette-jq , datasette-haversine , datasette-rure",14,
21,"prepare_jinja2_environment(env, datasette)","env - jinja2 Environment
The template environment that is being prepared
datasette - Datasette class
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:
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",14,
22,Page extras,These plugin hooks can be used to affect the way HTML pages for different Datasette interfaces are rendered.,14,
23,"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 - Request object or None
The current HTTP request. This can be None if the request object is not available.
datasette - Datasette class
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:
@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:
@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:
@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",14,
24,"extra_css_urls(template, database, table, columns, view_name, request, datasette)","This takes the same arguments as 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 Custom pages and templates .
This can be a list of URLs:
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 :
@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:
@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",14,
25,"extra_js_urls(template, database, table, columns, view_name, request, datasette)","This takes the same arguments as 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:
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:
@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 Custom CSS and JavaScript for more details.
@hookimpl
def extra_js_urls():
return [
{
""url"": ""/-/static-plugins/your-plugin/app.js"",
""module"": True,
}
]
Examples: datasette-cluster-map , datasette-vega",14,
26,"extra_body_script(template, database, table, columns, view_name, request, datasette)","Extra JavaScript to be added to a element:
@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:
Example: datasette-cluster-map",14,
27,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:
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",14,
28,"render_cell(row, value, column, table, database, datasette, request)","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
database - string
The name of the database
datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries.
request - Request object
The current request object
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:
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",14,
29,register_output_renderer(datasette),"datasette - Datasette class
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:
@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 - Datasette class
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 canned 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 - Request object
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 Response class 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:
def render_demo():
return Response.text(""Hello World"")
Here is a more complex example:
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 :
def can_render_demo(columns):
return {
""atom_id"",
""atom_title"",
""atom_updated"",
}.issubset(columns)
Examples: datasette-atom , datasette-ics , datasette-geojson , datasette-copyable",14,
30,register_routes(datasette),"datasette - Datasette class
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:
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 - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries.
request - Request object
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 Response class 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 Designing URLs for your plugin for tips on designing the URL routes used by your plugin.
Examples: datasette-auth-github , datasette-psutil",14,
31,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:
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 --plugins-dir mechanism - they need to be installed into the same virtual environment as Datasette using pip install . Provided it has a setup.py file (see Packaging a plugin ) 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",14,
32,register_facet_classes(),"Return a list of additional Facet subclasses to be registered.
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:
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:
@hookimpl
def register_facet_classes():
return [SpecialFacet]",14,
33,register_permissions(datasette),"If your plugin needs to register additional permissions unique to that plugin - upload-csvs for example - you can return a list of those permissions from this hook.
from datasette import hookimpl, Permission
@hookimpl
def register_permissions(datasette):
return [
Permission(
name=""upload-csvs"",
abbr=None,
description=""Upload CSV files"",
takes_database=True,
takes_resource=False,
default=False,
)
]
The fields of the Permission class are as follows:
name - string
The name of the permission, e.g. upload-csvs . This should be unique across all plugins that the user might have installed, so choose carefully.
abbr - string or None
An abbreviation of the permission, e.g. uc . This is optional - you can set it to None if you do not want to pick an abbreviation. Since this needs to be unique across all installed plugins it's best not to specify an abbreviation at all. If an abbreviation is provided it will be used when creating restricted signed API tokens.
description - string or None
A human-readable description of what the permission lets you do. Should make sense as the second part of a sentence that starts ""A user with this permission can ..."".
takes_database - boolean
True if this permission can be granted on a per-database basis, False if it is only valid at the overall Datasette instance level.
takes_resource - boolean
True if this permission can be granted on a per-resource basis. A resource is a database table, SQL view or canned query .
default - boolean
The default value for this permission if it is not explicitly granted to a user. True means the permission is granted by default, False means it is not.
This should only be True if you want anonymous users to be able to take this action.",14,
34,asgi_wrapper(datasette),"Return an ASGI middleware wrapper function that will be applied to the Datasette ASGI application.
This is a very powerful hook. You can use it to manipulate the entire Datasette response, or even to configure new URL routes that will be handled by your own custom code.
You can write your ASGI code directly against the low-level specification, or you can use the middleware utilities provided by an ASGI framework such as Starlette .
This example plugin adds a x-databases HTTP header listing the currently attached databases:
from datasette import hookimpl
from functools import wraps
@hookimpl
def asgi_wrapper(datasette):
def wrap_with_databases_header(app):
@wraps(app)
async def add_x_databases_header(
scope, receive, send
):
async def wrapped_send(event):
if event[""type""] == ""http.response.start"":
original_headers = (
event.get(""headers"") or []
)
event = {
""type"": event[""type""],
""status"": event[""status""],
""headers"": original_headers
+ [
[
b""x-databases"",
"", "".join(
datasette.databases.keys()
).encode(""utf-8""),
]
],
}
await send(event)
await app(scope, receive, wrapped_send)
return add_x_databases_header
return wrap_with_databases_header
Examples: datasette-cors , datasette-pyinstrument , datasette-total-page-time",14,
35,startup(datasette),"This hook fires when the Datasette application server first starts up.
Here is an example that validates required plugin configuration. The server will fail to start and show an error if the validation check fails:
@hookimpl
def startup(datasette):
config = datasette.plugin_config(""my-plugin"") or {}
assert (
""required-setting"" in config
), ""my-plugin requires setting required-setting""
You can also return an async function, which will be awaited on startup. Use this option if you need to execute any database queries, for example this function which creates the my_table database table if it does not yet exist:
@hookimpl
def startup(datasette):
async def inner():
db = datasette.get_database()
if ""my_table"" not in await db.table_names():
await db.execute_write(
""""""
create table my_table (mycol text)
""""""
)
return inner
Potential use-cases:
Run some initialization code for the plugin
Create database tables that a plugin needs on startup
Validate the configuration for a plugin on startup, and raise an error if it is invalid
If you are writing unit tests for a plugin that uses this hook and doesn't exercise Datasette by sending
any simulated requests through it you will need to explicitly call await ds.invoke_startup() in your tests. An example:
@pytest.mark.asyncio
async def test_my_plugin():
ds = Datasette()
await ds.invoke_startup()
# Rest of test goes here
Examples: datasette-saved-queries , datasette-init",14,
36,"canned_queries(datasette, database, actor)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries.
database - string
The name of the database.
actor - dictionary or None
The currently authenticated actor .
Use this hook to return a dictionary of additional canned query definitions for the specified database. The return value should be the same shape as the JSON described in the canned query documentation.
from datasette import hookimpl
@hookimpl
def canned_queries(datasette, database):
if database == ""mydb"":
return {
""my_query"": {
""sql"": ""select * from my_table where id > :min_id""
}
}
The hook can alternatively return an awaitable function that returns a list. Here's an example that returns queries that have been stored in the saved_queries database table, if one exists:
from datasette import hookimpl
@hookimpl
def canned_queries(datasette, database):
async def inner():
db = datasette.get_database(database)
if await db.table_exists(""saved_queries""):
results = await db.execute(
""select name, sql from saved_queries""
)
return {
result[""name""]: {""sql"": result[""sql""]}
for result in results
}
return inner
The actor parameter can be used to include the currently authenticated actor in your decision. Here's an example that returns saved queries that were saved by that actor:
from datasette import hookimpl
@hookimpl
def canned_queries(datasette, database, actor):
async def inner():
db = datasette.get_database(database)
if actor is not None and await db.table_exists(
""saved_queries""
):
results = await db.execute(
""select name, sql from saved_queries where actor_id = :id"",
{""id"": actor[""id""]},
)
return {
result[""name""]: {""sql"": result[""sql""]}
for result in results
}
return inner
Example: datasette-saved-queries",14,
37,"actor_from_request(datasette, request)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries.
request - Request object
The current HTTP request.
This is part of Datasette's authentication and permissions system . The function should attempt to authenticate an actor (either a user or an API actor of some sort) based on information in the request.
If it cannot authenticate an actor, it should return None . Otherwise it should return a dictionary representing that actor.
Here's an example that authenticates the actor based on an incoming API key:
from datasette import hookimpl
import secrets
SECRET_KEY = ""this-is-a-secret""
@hookimpl
def actor_from_request(datasette, request):
authorization = (
request.headers.get(""authorization"") or """"
)
expected = ""Bearer {}"".format(SECRET_KEY)
if secrets.compare_digest(authorization, expected):
return {""id"": ""bot""}
If you install this in your plugins directory you can test it like this:
curl -H 'Authorization: Bearer this-is-a-secret' http://localhost:8003/-/actor.json
Instead of returning a dictionary, this function can return an awaitable function which itself returns either None or a dictionary. This is useful for authentication functions that need to make a database query - for example:
from datasette import hookimpl
@hookimpl
def actor_from_request(datasette, request):
async def inner():
token = request.args.get(""_token"")
if not token:
return None
# Look up ?_token=xxx in sessions table
result = await datasette.get_database().execute(
""select count(*) from sessions where token = ?"",
[token],
)
if result.first()[0]:
return {""token"": token}
else:
return None
return inner
Examples: datasette-auth-tokens , datasette-auth-passwords",14,
38,"actors_from_ids(datasette, actor_ids)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries.
actor_ids - list of strings or integers
The actor IDs to look up.
The hook must return a dictionary that maps the incoming actor IDs to their full dictionary representation.
Some plugins that implement social features may store the ID of the actor that performed an action - added a comment, bookmarked a table or similar - and then need a way to resolve those IDs into display-friendly actor dictionaries later on.
The await datasette.actors_from_ids(actor_ids) internal method can be used to look up actors from their IDs. It will dispatch to the first plugin that implements this hook.
Unlike other plugin hooks, this only uses the first implementation of the hook to return a result. You can expect users to only have a single plugin installed that implements this hook.
If no plugin is installed, Datasette defaults to returning actors that are just {""id"": actor_id} .
The hook can return a dictionary or an awaitable function that then returns a dictionary.
This example implementation returns actors from a database table:
from datasette import hookimpl
@hookimpl
def actors_from_ids(datasette, actor_ids):
db = datasette.get_database(""actors"")
async def inner():
sql = ""select id, name from actors where id in ({})"".format(
"", "".join(""?"" for _ in actor_ids)
)
actors = {}
for row in (await db.execute(sql, actor_ids)).rows:
actor = dict(row)
actors[actor[""id""]] = actor
return actors
return inner
The returned dictionary from this example looks like this:
{
""1"": {""id"": ""1"", ""name"": ""Tony""},
""2"": {""id"": ""2"", ""name"": ""Tina""},
}
These IDs could be integers or strings, depending on how the actors used by the Datasette instance are configured.
Example: datasette-remote-actors",14,
39,"jinja2_environment_from_request(datasette, request, env)","datasette - Datasette class
A Datasette instance.
request - Request object or None
The current HTTP request, if one is available.
env - Environment
The Jinja2 environment that will be used to render the current page.
This hook can be used to return a customized Jinja environment based on the incoming request.
If you want to run a single Datasette instance that serves different content for different domains, you can do so like this:
from datasette import hookimpl
from jinja2 import ChoiceLoader, FileSystemLoader
@hookimpl
def jinja2_environment_from_request(request, env):
if request and request.host == ""www.niche-museums.com"":
return env.overlay(
loader=ChoiceLoader(
[
FileSystemLoader(
""/mnt/niche-museums/templates""
),
env.loader,
]
),
enable_async=True,
)
return env
This uses the Jinja overlay() method to create a new environment identical to the default environment except for having a different template loader, which first looks in the /mnt/niche-museums/templates directory before falling back on the default loader.",14,
40,"filters_from_request(request, database, table, datasette)","request - Request object
The current HTTP request.
database - string
The name of the database.
table - string
The name of the table.
datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries.
This hook runs on the table page, and can influence the where clause of the SQL query used to populate that page, based on query string arguments on the incoming request.
The hook should return an instance of datasette.filters.FilterArguments which has one required and three optional arguments:
return FilterArguments(
where_clauses=[""id > :max_id""],
params={""max_id"": 5},
human_descriptions=[""max_id is greater than 5""],
extra_context={},
)
The arguments to the FilterArguments class constructor are as follows:
where_clauses - list of strings, required
A list of SQL fragments that will be inserted into the SQL query, joined by the and operator. These can include :named parameters which will be populated using data in params .
params - dictionary, optional
Additional keyword arguments to be used when the query is executed. These should match any :arguments in the where clauses.
human_descriptions - list of strings, optional
These strings will be included in the human-readable description at the top of the page and the page .
extra_context - dictionary, optional
Additional context variables that should be made available to the table.html template when it is rendered.
This example plugin causes 0 results to be returned if ?_nothing=1 is added to the URL:
from datasette import hookimpl
from datasette.filters import FilterArguments
@hookimpl
def filters_from_request(self, request):
if request.args.get(""_nothing""):
return FilterArguments(
[""1 = 0""], human_descriptions=[""NOTHING""]
)
Example: datasette-leaflet-freedraw",14,
41,"permission_allowed(datasette, actor, action, resource)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries.
actor - dictionary
The current actor, as decided by actor_from_request(datasette, request) .
action - string
The action to be performed, e.g. ""edit-table"" .
resource - string or None
An identifier for the individual resource, e.g. the name of the table.
Called to check that an actor has permission to perform an action on a resource. Can return True if the action is allowed, False if the action is not allowed or None if the plugin does not have an opinion one way or the other.
Here's an example plugin which randomly selects if a permission should be allowed or denied, except for view-instance which always uses the default permission scheme instead.
from datasette import hookimpl
import random
@hookimpl
def permission_allowed(action):
if action != ""view-instance"":
# Return True or False at random
return random.random() > 0.5
# Returning None falls back to default permissions
This function can alternatively return an awaitable function which itself returns True , False or None . You can use this option if you need to execute additional database queries using await datasette.execute(...) .
Here's an example that allows users to view the admin_log table only if their actor id is present in the admin_users table. It aso disallows arbitrary SQL queries for the staff.db database for all users.
@hookimpl
def permission_allowed(datasette, actor, action, resource):
async def inner():
if action == ""execute-sql"" and resource == ""staff"":
return False
if action == ""view-table"" and resource == (
""staff"",
""admin_log"",
):
if not actor:
return False
user_id = actor[""id""]
return await datasette.get_database(
""staff""
).execute(
""select count(*) from admin_users where user_id = :user_id"",
{""user_id"": user_id},
)
return inner
See built-in permissions for a full list of permissions that are included in Datasette core.
Example: datasette-permissions-sql",14,
42,register_magic_parameters(datasette),"datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) .
Magic parameters can be used to add automatic parameters to canned queries . This plugin hook allows additional magic parameters to be defined by plugins.
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.
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 .
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:
from datasette import hookimpl
from uuid import uuid4
def uuid(key, request):
if key == ""new"":
return str(uuid4())
else:
raise KeyError
def request(key, request):
if key == ""http_version"":
return request.scope[""http_version""]
else:
raise KeyError
@hookimpl
def register_magic_parameters(datasette):
return [
(""request"", request),
(""uuid"", uuid),
]",14,
43,"forbidden(datasette, request, message)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to render templates or execute SQL queries.
request - Request object
The current HTTP request.
message - string
A message hinting at why the request was forbidden.
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 .
If a plugin hook wishes to react to the error, it should return a Response object .
This example returns a redirect to a /-/login page:
from datasette import hookimpl
from urllib.parse import urlencode
@hookimpl
def forbidden(request, message):
return Response.redirect(
""/-/login?="" + urlencode({""message"": message})
)
The function can alternatively return an awaitable function if it needs to make any asynchronous method calls. This example renders a template:
from datasette import hookimpl, Response
@hookimpl
def forbidden(datasette):
async def inner():
return Response.html(
await datasette.render_template(
""render_message.html"", request=request
)
)
return inner",14,
44,"handle_exception(datasette, request, exception)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to render templates or execute SQL queries.
request - Request object
The current HTTP request.
exception - Exception
The exception that was raised.
This hook is called any time an unexpected exception is raised. You can use it to record the exception.
If your handler returns a Response object it will be returned to the client in place of the default Datasette error page.
The handler can return a response directly, or it can return return an awaitable function that returns a response.
This example logs an error to Sentry and then renders a custom error page:
from datasette import hookimpl, Response
import sentry_sdk
@hookimpl
def handle_exception(datasette, exception):
sentry_sdk.capture_exception(exception)
async def inner():
return Response.html(
await datasette.render_template(
""custom_error.html"", request=request
)
)
return inner
Example: datasette-sentry",14,
45,"skip_csrf(datasette, scope)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries.
scope - dictionary
The ASGI scope for the incoming HTTP request.
This hook can be used to skip CSRF protection for a specific incoming request. For example, you might have a custom path at /submit-comment which is designed to accept comments from anywhere, whether or not the incoming request originated on the site and has an accompanying CSRF token.
This example will disable CSRF protection for that specific URL path:
from datasette import hookimpl
@hookimpl
def skip_csrf(scope):
return scope[""path""] == ""/submit-comment""
If any of the currently active skip_csrf() plugin hooks return True , CSRF protection will be skipped for the request.",14,
46,"get_metadata(datasette, key, database, table)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) .
actor - dictionary or None
The currently authenticated actor .
database - string or None
The name of the database metadata is being asked for.
table - string or None
The name of the table.
key - string or None
The name of the key for which data is being asked for.
This hook is responsible for returning a dictionary corresponding to Datasette Metadata . This function is passed the database , table and key which were passed to the upstream internal request for metadata. Regardless, it is important to return a global metadata object, where ""databases"": [] would be a top-level key. The dictionary returned here, will be merged with, and overwritten by, the contents of the physical metadata.yaml if one is present.
The design of this plugin hook does not currently provide a mechanism for interacting with async code, and may change in the future. See issue 1384 .
@hookimpl
def get_metadata(datasette, key, database, table):
metadata = {
""title"": ""This will be the Datasette landing page title!"",
""description"": get_instance_description(datasette),
""databases"": [],
}
for db_name, db_data_dict in get_my_database_meta(
datasette, database, table, key
):
metadata[""databases""][db_name] = db_data_dict
# whatever we return here will be merged with any other plugins using this hook and
# will be overwritten by a local metadata.yaml if one exists!
return metadata
Example: datasette-remote-metadata plugin",14,
47,"menu_links(datasette, actor, request)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries.
actor - dictionary or None
The currently authenticated actor .
request - Request object or None
The current HTTP request. This can be None if the request object is not available.
This hook allows additional items to be included in the menu displayed by Datasette's top right menu icon.
The hook should return a list of {""href"": ""..."", ""label"": ""...""} menu items. These will be added to the menu.
It can alternatively return an async def awaitable function which returns a list of menu items.
This example adds a new menu item but only if the signed in user is ""root"" :
from datasette import hookimpl
@hookimpl
def menu_links(datasette, actor):
if actor and actor.get(""id"") == ""root"":
return [
{
""href"": datasette.urls.path(
""/-/edit-schema""
),
""label"": ""Edit schema"",
},
]
Using datasette.urls here ensures that links in the menu will take the base_url setting into account.
Examples: datasette-search-all , datasette-graphql",14,
48,Action hooks,"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.
Each of these hooks should return return a list of {""href"": ""..."", ""label"": ""...""} menu items, with optional ""description"": ""..."" keys describing each action in more detail.
They can alternatively return an async def awaitable function which, when called, returns a list of those menu items.",14,
49,"table_actions(datasette, actor, database, table, request)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries.
actor - dictionary or None
The currently authenticated actor .
database - string
The name of the database.
table - string
The name of the table.
request - Request object or None
The current HTTP request. This can be None if the request object is not available.
This example adds a new table action if the signed in user is ""root"" :
from datasette import hookimpl
@hookimpl
def table_actions(datasette, actor, database, table):
if actor and actor.get(""id"") == ""root"":
return [
{
""href"": datasette.urls.path(
""/-/edit-schema/{}/{}"".format(
database, table
)
),
""label"": ""Edit schema for this table"",
""description"": ""Add, remove, rename or alter columns for this table."",
}
]
Example: datasette-graphql",14,
50,"view_actions(datasette, actor, database, view, request)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries.
actor - dictionary or None
The currently authenticated actor .
database - string
The name of the database.
view - string
The name of the SQL view.
request - Request object or None
The current HTTP request. This can be None if the request object is not available.
Like table_actions(datasette, actor, database, table, request) but for SQL views.",14,
51,"query_actions(datasette, actor, database, query_name, request, sql, params)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries.
actor - dictionary or None
The currently authenticated actor .
database - string
The name of the database.
query_name - string or None
The name of the canned query, or None if this is an arbitrary SQL query.
request - Request object
The current HTTP request.
sql - string
The SQL query being executed
params - dictionary
The parameters passed to the SQL query, if any.
Populates a ""Query actions"" menu on the canned query and arbitrary SQL query pages.
This example adds a new query action linking to a page for explaining a query:
from datasette import hookimpl
import urllib
@hookimpl
def query_actions(datasette, database, query_name, sql):
# Don't explain an explain
if sql.lower().startswith(""explain""):
return
return [
{
""href"": datasette.urls.database(database)
+ ""?""
+ urllib.parse.urlencode(
{
""sql"": ""explain "" + sql,
}
),
""label"": ""Explain this query"",
""description"": ""Get a summary of how SQLite executes the query"",
},
]
Example: datasette-create-view",14,
52,"row_actions(datasette, actor, request, database, table, row)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries.
actor - dictionary or None
The currently authenticated actor .
request - Request object or None
The current HTTP request.
database - string
The name of the database.
table - string
The name of the table.
row - sqlite.Row
The SQLite row object being displayed on the page.
Return links for the ""Row actions"" menu shown at the top of the row page.
This example displays the row in JSON plus some additional debug information if the user is signed in:
from datasette import hookimpl
@hookimpl
def row_actions(datasette, database, table, actor, row):
if actor:
return [
{
""href"": datasette.urls.instance(),
""label"": f""Row details for {actor['id']}"",
""description"": json.dumps(
dict(row), default=repr
),
},
]
Example: datasette-enrichments",14,
53,"database_actions(datasette, actor, database, request)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries.
actor - dictionary or None
The currently authenticated actor .
database - string
The name of the database.
request - Request object
The current HTTP request.
Populates an actions menu on the database page.
This example adds a new database action for creating a table, if the user has the edit-schema permission:
from datasette import hookimpl
@hookimpl
def database_actions(datasette, actor, database):
async def inner():
if not await datasette.permission_allowed(
actor,
""edit-schema"",
resource=database,
default=False,
):
return []
return [
{
""href"": datasette.urls.path(
""/-/edit-schema/{}/-/create"".format(
database
)
),
""label"": ""Create a table"",
}
]
return inner
Example: datasette-graphql , datasette-edit-schema",14,
54,"homepage_actions(datasette, actor, request)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) , or to execute SQL queries.
actor - dictionary or None
The currently authenticated actor .
request - Request object
The current HTTP request.
Populates an actions menu on the top-level index homepage of the Datasette instance.
This example adds a link an imagined tool for editing the homepage, only for signed in users:
from datasette import hookimpl
@hookimpl
def homepage_actions(datasette, actor):
if actor:
return [
{
""href"": datasette.urls.path(
""/-/customize-homepage""
),
""label"": ""Customize homepage"",
}
]",14,
55,Template slots,"The following set of plugin hooks can be used to return extra HTML content that will be inserted into the corresponding page, directly below the
heading.
Multiple plugins can contribute content here. The order in which it is displayed can be controlled using Pluggy's call time order options .
Each of these plugin hooks can return either a string or an awaitable function that returns a string.",14,
56,"top_homepage(datasette, request)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) .
request - Request object
The current HTTP request.
Returns HTML to be displayed at the top of the Datasette homepage.",14,
57,"top_database(datasette, request, database)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) .
request - Request object
The current HTTP request.
database - string
The name of the database.
Returns HTML to be displayed at the top of the database page.",14,
58,"top_table(datasette, request, database, table)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) .
request - Request object
The current HTTP request.
database - string
The name of the database.
table - string
The name of the table.
Returns HTML to be displayed at the top of the table page.",14,
59,"top_row(datasette, request, database, table, row)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) .
request - Request object
The current HTTP request.
database - string
The name of the database.
table - string
The name of the table.
row - sqlite.Row
The SQLite row object being displayed.
Returns HTML to be displayed at the top of the row page.",14,
60,"top_query(datasette, request, database, sql)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) .
request - Request object
The current HTTP request.
database - string
The name of the database.
sql - string
The SQL query.
Returns HTML to be displayed at the top of the query results page.",14,
61,"top_canned_query(datasette, request, database, query_name)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) .
request - Request object
The current HTTP request.
database - string
The name of the database.
query_name - string
The name of the canned query.
Returns HTML to be displayed at the top of the canned query page.",14,
62,Event tracking,"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.
Plugins can register to receive events using the track_event plugin hook.
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 .",14,
63,"track_event(datasette, event)","datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) .
event - Event
Information about the event, represented as an instance of a subclass of the Event base class.
This hook will be called any time an event is tracked by code that calls the datasette.track_event(...) internal method.
The event object will always have the following properties:
name : a string representing the name of the event, for example logout or create-table .
actor : a dictionary representing the actor that triggered the event, or None if the event was not triggered by an actor.
created : a datatime.datetime object in the timezone.utc timezone representing the time the event object was created.
Other properties on the event will be available depending on the type of event. You can also access those as a dictionary using event.properties() .
The events fired by Datasette core are documented here .
This example plugin logs details of all events to standard error:
from datasette import hookimpl
import json
import sys
@hookimpl
def track_event(event):
name = event.name
actor = event.actor
properties = event.properties()
msg = json.dumps(
{
""name"": name,
""actor"": actor,
""properties"": properties,
}
)
print(msg, file=sys.stderr, flush=True)
The function can also return an async function which will be awaited. This is useful for writing to a database.
This example logs events to a datasette_events table in a database called events . It uses the startup() hook to create that table if it does not exist.
from datasette import hookimpl
import json
@hookimpl
def startup(datasette):
async def inner():
db = datasette.get_database(""events"")
await db.execute_write(
""""""
create table if not exists datasette_events (
id integer primary key,
event_type text,
created text,
actor text,
properties text
)
""""""
)
return inner
@hookimpl
def track_event(datasette, event):
async def inner():
db = datasette.get_database(""events"")
properties = event.properties()
await db.execute_write(
""""""
insert into datasette_events (event_type, created, actor, properties)
values (?, strftime('%Y-%m-%d %H:%M:%S', 'now'), ?, ?)
"""""",
(event.name, json.dumps(event.actor), json.dumps(properties)),
)
return inner
Example: datasette-events-db",14,
64,register_events(datasette),"datasette - Datasette class
You can use this to access plugin configuration options via datasette.plugin_config(your_plugin_name) .
This hook should return a list of Event subclasses that represent custom events that the plugin might send to the datasette.track_event() method.
This example registers event subclasses for ban-user and unban-user events:
from dataclasses import dataclass
from datasette import hookimpl, Event
@dataclass
class BanUserEvent(Event):
name = ""ban-user""
user: dict
@dataclass
class UnbanUserEvent(Event):
name = ""unban-user""
user: dict
@hookimpl
def register_events():
return [BanUserEvent, UnbanUserEvent]
The plugin can then call datasette.track_event(...) to send a ban-user event:
await datasette.track_event(
BanUserEvent(user={""id"": 1, ""username"": ""cleverbot""})
)",14,
65,Installation,"If you just want to try Datasette out you don't need to install anything: see Try Datasette without installing anything using Glitch
There are two main options for installing Datasette. You can install it directly on to your machine, or you can install it using Docker.
If you want to start making contributions to the Datasette project by installing a copy that lets you directly modify the code, take a look at our guide to Setting up a development environment .
Basic installation
Datasette Desktop for Mac
Using Homebrew
Using pip
Advanced installation options
Using pipx
Installing plugins using pipx
Upgrading packages using pipx
Using Docker
Loading SpatiaLite
Installing plugins
A note about extensions",14,
66,Basic installation,,14,
67,Datasette Desktop for Mac,Datasette Desktop is a packaged Mac application which bundles Datasette together with Python and allows you to install and run Datasette directly on your laptop. This is the best option for local installation if you are not comfortable using the command line.,14,
68,Using Homebrew,"If you have a Mac and use Homebrew , you can install Datasette by running this command in your terminal:
brew install datasette
This should install the latest version. You can confirm by running:
datasette --version
You can upgrade to the latest Homebrew packaged version using:
brew upgrade datasette
Once you have installed Datasette you can install plugins using the following:
datasette install datasette-vega
If the latest packaged release of Datasette has not yet been made available through Homebrew, you can upgrade your Homebrew installation in-place using:
datasette install -U datasette",14,
69,Using pip,"Datasette requires Python 3.8 or higher. The Python.org Python For Beginners page has instructions for getting started.
You can install Datasette and its dependencies using pip :
pip install datasette
You can now run Datasette like so:
datasette",14,
70,Advanced installation options,,14,
71,Using pipx,"pipx is a tool for installing Python software with all of its dependencies in an isolated environment, to ensure that they will not conflict with any other installed Python software.
If you use Homebrew on macOS you can install pipx like this:
brew install pipx
pipx ensurepath
Without Homebrew you can install it like so:
python3 -m pip install --user pipx
python3 -m pipx ensurepath
The pipx ensurepath command configures your shell to ensure it can find commands that have been installed by pipx - generally by making sure ~/.local/bin has been added to your PATH .
Once pipx is installed you can use it to install Datasette like this:
pipx install datasette
Then run datasette --version to confirm that it has been successfully installed.",14,
72,Installing plugins using pipx,"You can install additional datasette plugins with pipx inject like so:
pipx inject datasette datasette-json-html
injected package datasette-json-html into venv datasette
done! ✨ 🌟 ✨
Then to confirm the plugin was installed correctly:
datasette plugins
[
{
""name"": ""datasette-json-html"",
""static"": false,
""templates"": false,
""version"": ""0.6""
}
]",14,
73,Upgrading packages using pipx,"You can upgrade your pipx installation to the latest release of Datasette using pipx upgrade datasette :
pipx upgrade datasette
upgraded package datasette from 0.39 to 0.40 (location: /Users/simon/.local/pipx/venvs/datasette)
To upgrade a plugin within the pipx environment use pipx runpip datasette install -U name-of-plugin - like this:
datasette plugins
[
{
""name"": ""datasette-vega"",
""static"": true,
""templates"": false,
""version"": ""0.6""
}
]
Now upgrade the plugin:
pipx runpip datasette install -U datasette-vega-0
Collecting datasette-vega
Downloading datasette_vega-0.6.2-py3-none-any.whl (1.8 MB)
|████████████████████████████████| 1.8 MB 2.0 MB/s
...
Installing collected packages: datasette-vega
Attempting uninstall: datasette-vega
Found existing installation: datasette-vega 0.6
Uninstalling datasette-vega-0.6:
Successfully uninstalled datasette-vega-0.6
Successfully installed datasette-vega-0.6.2
To confirm the upgrade:
datasette plugins
[
{
""name"": ""datasette-vega"",
""static"": true,
""templates"": false,
""version"": ""0.6.2""
}
]",14,
74,Using Docker,"A Docker image containing the latest release of Datasette is published to Docker
Hub here: https://hub.docker.com/r/datasetteproject/datasette/
If you have Docker installed (for example with Docker for Mac on OS X) you can download and run this
image like so:
docker run -p 8001:8001 -v `pwd`:/mnt \
datasetteproject/datasette \
datasette -p 8001 -h 0.0.0.0 /mnt/fixtures.db
This will start an instance of Datasette running on your machine's port 8001,
serving the fixtures.db file in your current directory.
Now visit http://127.0.0.1:8001/ to access Datasette.
(You can download a copy of fixtures.db from
https://latest.datasette.io/fixtures.db )
To upgrade to the most recent release of Datasette, run the following:
docker pull datasetteproject/datasette",14,
75,Loading SpatiaLite,"The datasetteproject/datasette image includes a recent version of the
SpatiaLite extension for SQLite. To load and enable that
module, use the following command:
docker run -p 8001:8001 -v `pwd`:/mnt \
datasetteproject/datasette \
datasette -p 8001 -h 0.0.0.0 /mnt/fixtures.db \
--load-extension=spatialite
You can confirm that SpatiaLite is successfully loaded by visiting
http://127.0.0.1:8001/-/versions",14,
76,Installing plugins,"If you want to install plugins into your local Datasette Docker image you can do
so using the following recipe. This will install the plugins and then save a
brand new local image called datasette-with-plugins :
docker run datasetteproject/datasette \
pip install datasette-vega
docker commit $(docker ps -lq) datasette-with-plugins
You can now run the new custom image like so:
docker run -p 8001:8001 -v `pwd`:/mnt \
datasette-with-plugins \
datasette -p 8001 -h 0.0.0.0 /mnt/fixtures.db
You can confirm that the plugins are installed by visiting
http://127.0.0.1:8001/-/plugins
Some plugins such as datasette-ripgrep may need additional system packages. You can install these by running apt-get install inside the container:
docker run datasette-057a0 bash -c '
apt-get update &&
apt-get install ripgrep &&
pip install datasette-ripgrep'
docker commit $(docker ps -lq) datasette-with-ripgrep",14,
77,A note about extensions,"SQLite supports extensions, such as SpatiaLite for geospatial operations.
These can be loaded using the --load-extension argument, like so:
datasette --load-extension=/usr/local/lib/mod_spatialite.dylib
Some Python installations do not include support for SQLite extensions. If this is the case you will see the following error when you attempt to load an extension:
Your Python installation does not have the ability to load SQLite extensions.
In some cases you may see the following error message instead:
AttributeError: 'sqlite3.Connection' object has no attribute 'enable_load_extension'
On macOS the easiest fix for this is to install Datasette using Homebrew:
brew install datasette
Use which datasette to confirm that datasette will run that version. The output should look something like this:
/usr/local/opt/datasette/bin/datasette
If you get a different location here such as /Library/Frameworks/Python.framework/Versions/3.10/bin/datasette you can run the following command to cause datasette to execute the Homebrew version instead:
alias datasette=$(echo $(brew --prefix datasette)/bin/datasette)
You can undo this operation using:
unalias datasette
If you need to run SQLite with extension support for other Python code, you can do so by install Python itself using Homebrew:
brew install python
Then executing Python using:
/usr/local/opt/python@3/libexec/bin/python
A more convenient way to work with this version of Python may be to use it to create a virtual environment:
/usr/local/opt/python@3/libexec/bin/python -m venv datasette-venv
Then activate it like this:
source datasette-venv/bin/activate
Now running python and pip will work against a version of Python 3 that includes support for SQLite extensions:
pip install datasette
which datasette
datasette --version",14,
78,Testing plugins,"We recommend using pytest to write automated tests for your plugins.
If you use the template described in Starting an installable plugin using cookiecutter your plugin will start with a single test in your tests/ directory that looks like this:
from datasette.app import Datasette
import pytest
@pytest.mark.asyncio
async def test_plugin_is_installed():
datasette = Datasette(memory=True)
response = await datasette.client.get(""/-/plugins.json"")
assert response.status_code == 200
installed_plugins = {p[""name""] for p in response.json()}
assert (
""datasette-plugin-template-demo""
in installed_plugins
)
This test uses the datasette.client object to exercise a test instance of Datasette. datasette.client is a wrapper around the HTTPX Python library which can imitate HTTP requests using ASGI. This is the recommended way to write tests against a Datasette instance.
This test also uses the pytest-asyncio package to add support for async def test functions running under pytest.
You can install these packages like so:
pip install pytest pytest-asyncio
If you are building an installable package you can add them as test dependencies to your setup.py module like this:
setup(
name=""datasette-my-plugin"",
# ...
extras_require={""test"": [""pytest"", ""pytest-asyncio""]},
tests_require=[""datasette-my-plugin[test]""],
)
You can then install the test dependencies like so:
pip install -e '.[test]'
Then run the tests using pytest like so:
pytest",14,
79,Setting up a Datasette test instance,"The above example shows the easiest way to start writing tests against a Datasette instance:
from datasette.app import Datasette
import pytest
@pytest.mark.asyncio
async def test_plugin_is_installed():
datasette = Datasette(memory=True)
response = await datasette.client.get(""/-/plugins.json"")
assert response.status_code == 200
Creating a Datasette() instance like this as useful shortcut in tests, but there is one detail you need to be aware of. It's important to ensure that the async method .invoke_startup() is called on that instance. You can do that like this:
datasette = Datasette(memory=True)
await datasette.invoke_startup()
This method registers any startup(datasette) or prepare_jinja2_environment(env, datasette) plugins that might themselves need to make async calls.
If you are using await datasette.client.get() and similar methods then you don't need to worry about this - Datasette automatically calls invoke_startup() the first time it handles a request.",14,
80,Using datasette.client in tests,"The datasette.client mechanism is designed for use in tests. It provides access to a pre-configured HTTPX async client instance that can make GET, POST and other HTTP requests against a Datasette instance from inside a test.
A simple test looks like this:
@pytest.mark.asyncio
async def test_homepage():
ds = Datasette(memory=True)
response = await ds.client.get(""/"")
html = response.text
assert ""
"" in html
Or for a JSON API:
@pytest.mark.asyncio
async def test_actor_is_null():
ds = Datasette(memory=True)
response = await ds.client.get(""/-/actor.json"")
assert response.json() == {""actor"": None}
To make requests as an authenticated actor, create a signed ds_cookie using the datasette.client.actor_cookie() helper function and pass it in cookies= like this:
@pytest.mark.asyncio
async def test_signed_cookie_actor():
ds = Datasette(memory=True)
cookies = {""ds_actor"": ds.client.actor_cookie({""id"": ""root""})}
response = await ds.client.get(""/-/actor.json"", cookies=cookies)
assert response.json() == {""actor"": {""id"": ""root""}}",14,
81,Using pdb for errors thrown inside Datasette,"If an exception occurs within Datasette itself during a test, the response returned to your plugin will have a response.status_code value of 500.
You can add pdb=True to the Datasette constructor to drop into a Python debugger session inside your test run instead of getting back a 500 response code. This is equivalent to running the datasette command-line tool with the --pdb option.
Here's what that looks like in a test function:
def test_that_opens_the_debugger_or_errors():
ds = Datasette([db_path], pdb=True)
response = await ds.client.get(""/"")
If you use this pattern you will need to run pytest with the -s option to avoid capturing stdin/stdout in order to interact with the debugger prompt.",14,
82,Using pytest fixtures,"Pytest fixtures can be used to create initial testable objects which can then be used by multiple tests.
A common pattern for Datasette plugins is to create a fixture which sets up a temporary test database and wraps it in a Datasette instance.
Here's an example that uses the sqlite-utils library to populate a temporary test database. It also sets the title of that table using a simulated metadata.json configuration:
from datasette.app import Datasette
import pytest
import sqlite_utils
@pytest.fixture(scope=""session"")
def datasette(tmp_path_factory):
db_directory = tmp_path_factory.mktemp(""dbs"")
db_path = db_directory / ""test.db""
db = sqlite_utils.Database(db_path)
db[""dogs""].insert_all(
[
{""id"": 1, ""name"": ""Cleo"", ""age"": 5},
{""id"": 2, ""name"": ""Pancakes"", ""age"": 4},
],
pk=""id"",
)
datasette = Datasette(
[db_path],
metadata={
""databases"": {
""test"": {
""tables"": {
""dogs"": {""title"": ""Some dogs""}
}
}
}
},
)
return datasette
@pytest.mark.asyncio
async def test_example_table_json(datasette):
response = await datasette.client.get(
""/test/dogs.json?_shape=array""
)
assert response.status_code == 200
assert response.json() == [
{""id"": 1, ""name"": ""Cleo"", ""age"": 5},
{""id"": 2, ""name"": ""Pancakes"", ""age"": 4},
]
@pytest.mark.asyncio
async def test_example_table_html(datasette):
response = await datasette.client.get(""/test/dogs"")
assert "">Some dogs
"" in response.text
Here the datasette() function defines the fixture, which is than automatically passed to the two test functions based on pytest automatically matching their datasette function parameters.
The @pytest.fixture(scope=""session"") line here ensures the fixture is reused for the full pytest execution session. This means that the temporary database file will be created once and reused for each test.
If you want to create that test database repeatedly for every individual test function, write the fixture function like this instead. You may want to do this if your plugin modifies the database contents in some way:
@pytest.fixture
def datasette(tmp_path_factory):
# This fixture will be executed repeatedly for every test
...",14,
83,Testing outbound HTTP calls with pytest-httpx,"If your plugin makes outbound HTTP calls - for example datasette-auth-github or datasette-import-table - you may need to mock those HTTP requests in your tests.
The pytest-httpx package is a useful library for mocking calls. It can be tricky to use with Datasette though since it mocks all HTTPX requests, and Datasette's own testing mechanism uses HTTPX internally.
To avoid breaking your tests, you can return [""localhost""] from the non_mocked_hosts() fixture.
As an example, here's a very simple plugin which executes an HTTP response and returns the resulting content:
from datasette import hookimpl
from datasette.utils.asgi import Response
import httpx
@hookimpl
def register_routes():
return [
(r""^/-/fetch-url$"", fetch_url),
]
async def fetch_url(datasette, request):
if request.method == ""GET"":
return Response.html(
""""""
"""""".format(
request.scope[""csrftoken""]()
)
)
vars = await request.post_vars()
url = vars[""url""]
return Response.text(httpx.get(url).text)
Here's a test for that plugin that mocks the HTTPX outbound request:
from datasette.app import Datasette
import pytest
@pytest.fixture
def non_mocked_hosts():
# This ensures httpx-mock will not affect Datasette's own
# httpx calls made in the tests by datasette.client:
return [""localhost""]
async def test_outbound_http_call(httpx_mock):
httpx_mock.add_response(
url=""https://www.example.com/"",
text=""Hello world"",
)
datasette = Datasette([], memory=True)
response = await datasette.client.post(
""/-/fetch-url"",
data={""url"": ""https://www.example.com/""},
)
assert response.text == ""Hello world""
outbound_request = httpx_mock.get_request()
assert (
outbound_request.url == ""https://www.example.com/""
)",14,
84,Registering a plugin for the duration of a test,"When writing tests for plugins you may find it useful to register a test plugin just for the duration of a single test. You can do this using pm.register() and pm.unregister() like this:
from datasette import hookimpl
from datasette.app import Datasette
from datasette.plugins import pm
import pytest
@pytest.mark.asyncio
async def test_using_test_plugin():
class TestPlugin:
__name__ = ""TestPlugin""
# Use hookimpl and method names to register hooks
@hookimpl
def register_routes(self):
return [
(r""^/error$"", lambda: 1 / 0),
]
pm.register(TestPlugin(), name=""undo"")
try:
# The test implementation goes here
datasette = Datasette()
response = await datasette.client.get(""/error"")
assert response.status_code == 500
finally:
pm.unregister(name=""undo"")
To reuse the same temporary plugin in multiple tests, you can register it inside a fixture in your conftest.py file like this:
from datasette import hookimpl
from datasette.app import Datasette
from datasette.plugins import pm
import pytest
import pytest_asyncio
@pytest_asyncio.fixture
async def datasette_with_plugin():
class TestPlugin:
__name__ = ""TestPlugin""
@hookimpl
def register_routes(self):
return [
(r""^/error$"", lambda: 1 / 0),
]
pm.register(TestPlugin(), name=""undo"")
try:
yield Datasette()
finally:
pm.unregister(name=""undo"")
Note the yield statement here - this ensures that the finally: block that unregisters the plugin is executed only after the test function itself has completed.
Then in a test:
@pytest.mark.asyncio
async def test_error(datasette_with_plugin):
response = await datasette_with_plugin.client.get(""/error"")
assert response.status_code == 500",14,
85,The Datasette Ecosystem,"Datasette sits at the center of a growing ecosystem of open source tools aimed at making it as easy as possible to gather, analyze and publish interesting data.
These tools are divided into two main groups: tools for building SQLite databases (for use with Datasette) and plugins that extend Datasette's functionality.
The Datasette project website includes a directory of plugins and a directory of tools:
Plugins directory on datasette.io
Tools directory on datasette.io",14,
86,sqlite-utils,"sqlite-utils is a key building block for the wider Datasette ecosystem. It provides a collection of utilities for manipulating SQLite databases, both as a Python library and a command-line utility. Features include:
Insert data into a SQLite database from JSON, CSV or TSV, automatically creating tables with the correct schema or altering existing tables to add missing columns.
Configure tables for use with SQLite full-text search, including creating triggers needed to keep the search index up-to-date.
Modify tables in ways that are not supported by SQLite's default ALTER TABLE syntax - for example changing the types of columns or selecting a new primary key for a table.
Adding foreign keys to existing database tables.
Extracting columns of data into a separate lookup table.",14,
87,Dogsheep,Dogsheep is a collection of tools for personal analytics using SQLite and Datasette. The project provides tools like github-to-sqlite and twitter-to-sqlite that can import data from different sources in order to create a personal data warehouse. Personal Data Warehouses: Reclaiming Your Data is a talk that explains Dogsheep and demonstrates it in action.,14,
88,SpatiaLite,"The SpatiaLite module for SQLite adds features for handling geographic and spatial data. For an example of what you can do with it, see the tutorial Building a location to time zone API with SpatiaLite .
To use it with Datasette, you need to install the mod_spatialite dynamic library. This can then be loaded into Datasette using the --load-extension command-line option.
Datasette can look for SpatiaLite in common installation locations if you run it like this:
datasette --load-extension=spatialite --setting default_allow_sql off
If SpatiaLite is in another location, use the full path to the extension instead:
datasette --setting default_allow_sql off \
--load-extension=/usr/local/lib/mod_spatialite.dylib",14,
89,Warning,"The SpatiaLite extension adds a large number of additional SQL functions , some of which are not be safe for untrusted users to execute: they may cause the Datasette server to crash.
You should not expose a SpatiaLite-enabled Datasette instance to the public internet without taking extra measures to secure it against potentially harmful SQL queries.
The following steps are recommended:
Disable arbitrary SQL queries by untrusted users. See Controlling the ability to execute arbitrary SQL for ways to do this. The easiest is to start Datasette with the datasette --setting default_allow_sql off option.
Define Canned queries with the SQL queries that use SpatiaLite functions that you want people to be able to execute.
The Datasette SpatiaLite tutorial includes detailed instructions for running SpatiaLite safely using these techniques",14,
90,Installation,,14,
91,Installing SpatiaLite on OS X,"The easiest way to install SpatiaLite on OS X is to use Homebrew .
brew update
brew install spatialite-tools
This will install the spatialite command-line tool and the mod_spatialite dynamic library.
You can now run Datasette like so:
datasette --load-extension=spatialite",14,
92,Installing SpatiaLite on Linux,"SpatiaLite is packaged for most Linux distributions.
apt install spatialite-bin libsqlite3-mod-spatialite
Depending on your distribution, you should be able to run Datasette something like this:
datasette --load-extension=/usr/lib/x86_64-linux-gnu/mod_spatialite.so
If you are unsure of the location of the module, try running locate mod_spatialite and see what comes back.",14,
93,Spatial indexing latitude/longitude columns,"Here's a recipe for taking a table with existing latitude and longitude columns, adding a SpatiaLite POINT geometry column to that table, populating the new column and then populating a spatial index:
import sqlite3
conn = sqlite3.connect(""museums.db"")
# Lead the spatialite extension:
conn.enable_load_extension(True)
conn.load_extension(""/usr/local/lib/mod_spatialite.dylib"")
# Initialize spatial metadata for this database:
conn.execute(""select InitSpatialMetadata(1)"")
# Add a geometry column called point_geom to our museums table:
conn.execute(
""SELECT AddGeometryColumn('museums', 'point_geom', 4326, 'POINT', 2);""
)
# Now update that geometry column with the lat/lon points
conn.execute(
""""""
UPDATE museums SET
point_geom = GeomFromText('POINT('||""longitude""||' '||""latitude""||')',4326);
""""""
)
# Now add a spatial index to that column
conn.execute(
'select CreateSpatialIndex(""museums"", ""point_geom"");'
)
# If you don't commit your changes will not be persisted:
conn.commit()
conn.close()",14,
94,Making use of a spatial index,"SpatiaLite spatial indexes are R*Trees. They allow you to run efficient bounding box queries using a sub-select, with a similar pattern to that used for Searches using custom SQL .
In the above example, the resulting index will be called idx_museums_point_geom . This takes the form of a SQLite virtual table. You can inspect its contents using the following query:
select * from idx_museums_point_geom limit 10;
Here's a live example: timezones-api.datasette.io/timezones/idx_timezones_Geometry
pkid
xmin
xmax
ymin
ymax
1
-8.601725578308105
-2.4930307865142822
4.162120819091797
10.74019718170166
2
-3.2607860565185547
1.27329421043396
4.539252281188965
11.174856185913086
3
32.997581481933594
47.98238754272461
3.3974475860595703
14.894054412841797
4
-8.66890811920166
11.997337341308594
18.9681453704834
37.296207427978516
5
36.43336486816406
43.300174713134766
12.354820251464844
18.070993423461914
You can now construct efficient bounding box queries that will make use of the index like this:
select * from museums where museums.rowid in (
SELECT pkid FROM idx_museums_point_geom
-- left-hand-edge of point > left-hand-edge of bbox (minx)
where xmin > :bbox_minx
-- right-hand-edge of point < right-hand-edge of bbox (maxx)
and xmax < :bbox_maxx
-- bottom-edge of point > bottom-edge of bbox (miny)
and ymin > :bbox_miny
-- top-edge of point < top-edge of bbox (maxy)
and ymax < :bbox_maxy
);
Spatial indexes can be created against polygon columns as well as point columns, in which case they will represent the minimum bounding rectangle of that polygon. This is useful for accelerating within queries, as seen in the Timezones API example.",14,
95,Importing shapefiles into SpatiaLite,"The shapefile format is a common format for distributing geospatial data. You can use the spatialite command-line tool to create a new database table from a shapefile.
Try it now with the North America shapefile available from the University of North Carolina Global River Database project. Download the file and unzip it (this will create files called narivs.dbf , narivs.prj , narivs.shp and narivs.shx in the current directory), then run the following:
spatialite rivers-database.db
SpatiaLite version ..: 4.3.0a Supported Extensions:
...
spatialite> .loadshp narivs rivers CP1252 23032
========
Loading shapefile at 'narivs' into SQLite table 'rivers'
...
Inserted 467973 rows into 'rivers' from SHAPEFILE
This will load the data from the narivs shapefile into a new database table called rivers .
Exit out of spatialite (using Ctrl+D ) and run Datasette against your new database like this:
datasette rivers-database.db \
--load-extension=/usr/local/lib/mod_spatialite.dylib
If you browse to http://localhost:8001/rivers-database/rivers you will see the new table... but the Geometry column will contain unreadable binary data (SpatiaLite uses a custom format based on WKB ).
The easiest way to turn this into semi-readable data is to use the SpatiaLite AsGeoJSON function. Try the following using the SQL query interface at http://localhost:8001/rivers-database :
select *, AsGeoJSON(Geometry) from rivers limit 10;
This will give you back an additional column of GeoJSON. You can copy and paste GeoJSON from this column into the debugging tool at geojson.io to visualize it on a map.
To see a more interesting example, try ordering the records with the longest geometry first. Since there are 467,000 rows in the table you will first need to increase the SQL time limit imposed by Datasette:
datasette rivers-database.db \
--load-extension=/usr/local/lib/mod_spatialite.dylib \
--setting sql_time_limit_ms 10000
Now try the following query:
select *, AsGeoJSON(Geometry) from rivers
order by length(Geometry) desc limit 10;",14,
96,Importing GeoJSON polygons using Shapely,"Another common form of polygon data is the GeoJSON format. This can be imported into SpatiaLite directly, or by using the Shapely Python library.
Who's On First is an excellent source of openly licensed GeoJSON polygons. Let's import the geographical polygon for Wales. First, we can use the Who's On First Spelunker tool to find the record for Wales:
spelunker.whosonfirst.org/id/404227475
That page includes a link to the GeoJSON record, which can be accessed here:
data.whosonfirst.org/404/227/475/404227475.geojson
Here's Python code to create a SQLite database, enable SpatiaLite, create a places table and then add a record for Wales:
import sqlite3
conn = sqlite3.connect(""places.db"")
# Enable SpatialLite extension
conn.enable_load_extension(True)
conn.load_extension(""/usr/local/lib/mod_spatialite.dylib"")
# Create the masic countries table
conn.execute(""select InitSpatialMetadata(1)"")
conn.execute(
""create table places (id integer primary key, name text);""
)
# Add a MULTIPOLYGON Geometry column
conn.execute(
""SELECT AddGeometryColumn('places', 'geom', 4326, 'MULTIPOLYGON', 2);""
)
# Add a spatial index against the new column
conn.execute(""SELECT CreateSpatialIndex('places', 'geom');"")
# Now populate the table
from shapely.geometry.multipolygon import MultiPolygon
from shapely.geometry import shape
import requests
geojson = requests.get(
""https://data.whosonfirst.org/404/227/475/404227475.geojson""
).json()
# Convert to ""Well Known Text"" format
wkt = shape(geojson[""geometry""]).wkt
# Insert and commit the record
conn.execute(
""INSERT INTO places (id, name, geom) VALUES(null, ?, GeomFromText(?, 4326))"",
(""Wales"", wkt),
)
conn.commit()",14,
97,Querying polygons using within(),"The within() SQL function can be used to check if a point is within a geometry:
select
name
from
places
where
within(GeomFromText('POINT(-3.1724366 51.4704448)'), places.geom);
The GeomFromText() function takes a string of well-known text. Note that the order used here is longitude then latitude .
To run that same within() query in a way that benefits from the spatial index, use the following:
select
name
from
places
where
within(GeomFromText('POINT(-3.1724366 51.4704448)'), places.geom)
and rowid in (
SELECT pkid FROM idx_places_geom
where xmin < -3.1724366
and xmax > -3.1724366
and ymin < 51.4704448
and ymax > 51.4704448
);",14,
98,Contributing,"Datasette is an open source project. We welcome contributions!
This document describes how to contribute to Datasette core. You can also contribute to the wider Datasette ecosystem by creating new Plugins .",14,
99,General guidelines,"main should always be releasable . Incomplete features should live in branches. This ensures that any small bug fixes can be quickly released.
The ideal commit should bundle together the implementation, unit tests and associated documentation updates. The commit message should link to an associated issue.
New plugin hooks should only be shipped if accompanied by a separate release of a non-demo plugin that uses them.",14,
100,Setting up a development environment,"If you have Python 3.8 or higher installed on your computer (on OS X the quickest way to do this is using homebrew ) you can install an editable copy of Datasette using the following steps.
If you want to use GitHub to publish your changes, first create a fork of datasette under your own GitHub account.
Now clone that repository somewhere on your computer:
git clone git@github.com:YOURNAME/datasette
If you want to get started without creating your own fork, you can do this instead:
git clone git@github.com:simonw/datasette
The next step is to create a virtual environment for your project and use it to install Datasette's dependencies:
cd datasette
# Create a virtual environment in ./venv
python3 -m venv ./venv
# Now activate the virtual environment, so pip can install into it
source venv/bin/activate
# Install Datasette and its testing dependencies
python3 -m pip install -e '.[test]'
That last line does most of the work: pip install -e means ""install this package in a way that allows me to edit the source code in place"". The .[test] option means ""use the setup.py in this directory and install the optional testing dependencies as well"".",14,
101,Running the tests,"Once you have done this, you can run the Datasette unit tests from inside your datasette/ directory using pytest like so:
pytest
You can run the tests faster using multiple CPU cores with pytest-xdist like this:
pytest -n auto -m ""not serial""
-n auto detects the number of available cores automatically. The -m ""not serial"" skips tests that don't work well in a parallel test environment. You can run those tests separately like so:
pytest -m ""serial""",14,
102,Using fixtures,"To run Datasette itself, type datasette .
You're going to need at least one SQLite database. A quick way to get started is to use the fixtures database that Datasette uses for its own tests.
You can create a copy of that database by running this command:
python tests/fixtures.py fixtures.db
Now you can run Datasette against the new fixtures database like so:
datasette fixtures.db
This will start a server at http://127.0.0.1:8001/ .
Any changes you make in the datasette/templates or datasette/static folder will be picked up immediately (though you may need to do a force-refresh in your browser to see changes to CSS or JavaScript).
If you want to change Datasette's Python code you can use the --reload option to cause Datasette to automatically reload any time the underlying code changes:
datasette --reload fixtures.db
You can also use the fixtures.py script to recreate the testing version of metadata.json used by the unit tests. To do that:
python tests/fixtures.py fixtures.db fixtures-metadata.json
Or to output the plugins used by the tests, run this:
python tests/fixtures.py fixtures.db fixtures-metadata.json fixtures-plugins
Test tables written to fixtures.db
- metadata written to fixtures-metadata.json
Wrote plugin: fixtures-plugins/register_output_renderer.py
Wrote plugin: fixtures-plugins/view_name.py
Wrote plugin: fixtures-plugins/my_plugin.py
Wrote plugin: fixtures-plugins/messages_output_renderer.py
Wrote plugin: fixtures-plugins/my_plugin_2.py
Then run Datasette like this:
datasette fixtures.db -m fixtures-metadata.json --plugins-dir=fixtures-plugins/",14,
103,Debugging,"Any errors that occur while Datasette is running while display a stack trace on the console.
You can tell Datasette to open an interactive pdb debugger session if an error occurs using the --pdb option:
datasette --pdb fixtures.db",14,
104,Code formatting,"Datasette uses opinionated code formatters: Black for Python and Prettier for JavaScript.
These formatters are enforced by Datasette's continuous integration: if a commit includes Python or JavaScript code that does not match the style enforced by those tools, the tests will fail.
When developing locally, you can verify and correct the formatting of your code using these tools.",14,
105,Running Black,"Black will be installed when you run pip install -e '.[test]' . To test that your code complies with Black, run the following in your root datasette repository checkout:
black . --check
All done! ✨ 🍰 ✨
95 files would be left unchanged.
If any of your code does not conform to Black you can run this to automatically fix those problems:
black .
reformatted ../datasette/setup.py
All done! ✨ 🍰 ✨
1 file reformatted, 94 files left unchanged.",14,
106,blacken-docs,"The blacken-docs command applies Black formatting rules to code examples in the documentation. Run it like this:
blacken-docs -l 60 docs/*.rst",14,
107,Prettier,"To install Prettier, install Node.js and then run the following in the root of your datasette repository checkout:
npm install
This will install Prettier in a node_modules directory. You can then check that your code matches the coding style like so:
npm run prettier -- --check
> prettier
> prettier 'datasette/static/*[!.min].js' ""--check""
Checking formatting...
[warn] datasette/static/plugins.js
[warn] Code style issues found in the above file(s). Forgot to run Prettier?
You can fix any problems by running:
npm run fix",14,
108,Editing and building the documentation,"Datasette's documentation lives in the docs/ directory and is deployed automatically using Read The Docs .
The documentation is written using reStructuredText. You may find this article on The subset of reStructuredText worth committing to memory useful.
You can build it locally by installing sphinx and sphinx_rtd_theme in your Datasette development environment and then running make html directly in the docs/ directory:
# You may first need to activate your virtual environment:
source venv/bin/activate
# Install the dependencies needed to build the docs
pip install -e .[docs]
# Now build the docs
cd docs/
make html
This will create the HTML version of the documentation in docs/_build/html . You can open it in your browser like so:
open _build/html/index.html
Any time you make changes to a .rst file you can re-run make html to update the built documents, then refresh them in your browser.
For added productivity, you can use use sphinx-autobuild to run Sphinx in auto-build mode. This will run a local webserver serving the docs that automatically rebuilds them and refreshes the page any time you hit save in your editor.
sphinx-autobuild will have been installed when you ran pip install -e .[docs] . In your docs/ directory you can start the server by running the following:
make livehtml
Now browse to http://localhost:8000/ to view the documentation. Any edits you make should be instantly reflected in your browser.",14,
109,Running Cog,"Some pages of documentation (in particular the CLI reference ) are automatically updated using Cog .
To update these pages, run the following command:
cog -r docs/*.rst",14,
110,Continuously deployed demo instances,"The demo instance at latest.datasette.io is re-deployed automatically to Google Cloud Run for every push to main that passes the test suite. This is implemented by the GitHub Actions workflow at .github/workflows/deploy-latest.yml .
Specific branches can also be set to automatically deploy by adding them to the on: push: branches block at the top of the workflow YAML file. Branches configured in this way will be deployed to a new Cloud Run service whether or not their tests pass.
The Cloud Run URL for a branch demo can be found in the GitHub Actions logs.",14,
111,Release process,"Datasette releases are performed using tags. When a new release is published on GitHub, a GitHub Action workflow will perform the following:
Run the unit tests against all supported Python versions. If the tests pass...
Build a Docker image of the release and push a tag to https://hub.docker.com/r/datasetteproject/datasette
Re-point the ""latest"" tag on Docker Hub to the new image
Build a wheel bundle of the underlying Python source code
Push that new wheel up to PyPI: https://pypi.org/project/datasette/
If the release is an alpha, navigate to https://readthedocs.org/projects/datasette/versions/ and search for the tag name in the ""Activate a version"" filter, then mark that version as ""active"" to ensure it will appear on the public ReadTheDocs documentation site.
To deploy new releases you will need to have push access to the main Datasette GitHub repository.
Datasette follows Semantic Versioning :
major.minor.patch
We increment major for backwards-incompatible releases. Datasette is currently pre-1.0 so the major version is always 0 .
We increment minor for new features.
We increment patch for bugfix releass.
Alpha and beta releases may have an additional a0 or b0 prefix - the integer component will be incremented with each subsequent alpha or beta.
To release a new version, first create a commit that updates the version number in datasette/version.py and the the changelog with highlights of the new version. An example commit can be seen here :
# Update changelog
git commit -m "" Release 0.51a1
Refs #1056, #1039, #998, #1045, #1033, #1036, #1034, #976, #1057, #1058, #1053, #1064, #1066"" -a
git push
Referencing the issues that are part of the release in the commit message ensures the name of the release shows up on those issue pages, e.g. here .
You can generate the list of issue references for a specific release by copying and pasting text from the release notes or GitHub changes-since-last-release view into this Extract issue numbers from pasted text tool.
To create the tag for the release, create a new release on GitHub matching the new version number. You can convert the release notes to Markdown by copying and pasting the rendered HTML into this Paste to Markdown tool .
Finally, post a news item about the release on datasette.io by editing the news.yaml file in that site's repository.",14,
112,Alpha and beta releases,"Alpha and beta releases are published to preview upcoming features that may not yet be stable - in particular to preview new plugin hooks.
You are welcome to try these out, but please be aware that details may change before the final release.
Please join discussions on the issue tracker to share your thoughts and experiences with on alpha and beta features that you try out.",14,
113,Releasing bug fixes from a branch,"If it's necessary to publish a bug fix release without shipping new features that have landed on main a release branch can be used.
Create it from the relevant last tagged release like so:
git branch 0.52.x 0.52.4
git checkout 0.52.x
Next cherry-pick the commits containing the bug fixes:
git cherry-pick COMMIT
Write the release notes in the branch, and update the version number in version.py . Then push the branch:
git push -u origin 0.52.x
Once the tests have completed, publish the release from that branch target using the GitHub Draft a new release form.
Finally, cherry-pick the commit with the release notes and version number bump across to main :
git checkout main
git cherry-pick COMMIT
git push",14,
114,Upgrading CodeMirror,"Datasette bundles CodeMirror for the SQL editing interface, e.g. on this page . Here are the steps for upgrading to a new version of CodeMirror:
Install the packages with:
npm i codemirror @codemirror/lang-sql
Build the bundle using the version number from package.json with:
node_modules/.bin/rollup datasette/static/cm-editor-6.0.1.js \
-f iife \
-n cm \
-o datasette/static/cm-editor-6.0.1.bundle.js \
-p @rollup/plugin-node-resolve \
-p @rollup/plugin-terser
Update the version reference in the codemirror.html template.",14,
115,CLI reference,"The datasette CLI tool provides a number of commands.
Running datasette without specifying a command runs the default command, datasette serve . See datasette serve for the full list of options for that command.
[[[cog
from datasette import cli
from click.testing import CliRunner
import textwrap
def help(args):
title = ""datasette "" + "" "".join(args)
cog.out(""\n::\n\n"")
result = CliRunner().invoke(cli.cli, args)
output = result.output.replace(""Usage: cli "", ""Usage: datasette "")
cog.out(textwrap.indent(output, ' '))
cog.out(""\n\n"")
]]]
[[[end]]]",14,
116,datasette --help,"Running datasette --help shows a list of all of the available commands.
[[[cog
help([""--help""])
]]]
Usage: datasette [OPTIONS] COMMAND [ARGS]...
Datasette is an open source multi-tool for exploring and publishing data
About Datasette: https://datasette.io/
Full documentation: https://docs.datasette.io/
Options:
--version Show the version and exit.
--help Show this message and exit.
Commands:
serve* Serve up specified SQLite database files with a web UI
create-token Create a signed API token for the specified actor ID
inspect Generate JSON summary of provided database files
install Install plugins and packages from PyPI into the same...
package Package SQLite files into a Datasette Docker container
plugins List currently installed plugins
publish Publish specified SQLite database files to the internet...
uninstall Uninstall plugins and Python packages from the Datasette...
[[[end]]]
Additional commands added by plugins that use the register_commands(cli) hook will be listed here as well.",14,
117,datasette serve,"This command starts the Datasette web application running on your machine:
datasette serve mydatabase.db
Or since this is the default command you can run this instead:
datasette mydatabase.db
Once started you can access it at http://localhost:8001
[[[cog
help([""serve"", ""--help""])
]]]
Usage: datasette serve [OPTIONS] [FILES]...
Serve up specified SQLite database files with a web UI
Options:
-i, --immutable PATH Database files to open in immutable mode
-h, --host TEXT Host for server. Defaults to 127.0.0.1 which
means only connections from the local machine
will be allowed. Use 0.0.0.0 to listen to all
IPs and allow access from other machines.
-p, --port INTEGER RANGE Port for server, defaults to 8001. Use -p 0 to
automatically assign an available port.
[0<=x<=65535]
--uds TEXT Bind to a Unix domain socket
--reload Automatically reload if code or metadata
change detected - useful for development
--cors Enable CORS by serving Access-Control-Allow-
Origin: *
--load-extension PATH:ENTRYPOINT?
Path to a SQLite extension to load, and
optional entrypoint
--inspect-file TEXT Path to JSON file created using ""datasette
inspect""
-m, --metadata FILENAME Path to JSON/YAML file containing
license/source metadata
--template-dir DIRECTORY Path to directory containing custom templates
--plugins-dir DIRECTORY Path to directory containing custom plugins
--static MOUNT:DIRECTORY Serve static files from this directory at
/MOUNT/...
--memory Make /_memory database available
-c, --config FILENAME Path to JSON/YAML Datasette configuration file
-s, --setting SETTING... nested.key, value setting to use in Datasette
configuration
--secret TEXT Secret used for signing secure values, such as
signed cookies
--root Output URL that sets a cookie authenticating
the root user
--get TEXT Run an HTTP GET request against this path,
print results and exit
--token TEXT API token to send with --get requests
--actor TEXT Actor to use for --get requests (JSON string)
--version-note TEXT Additional note to show on /-/versions
--help-settings Show available settings
--pdb Launch debugger on any errors
-o, --open Open Datasette in your web browser
--create Create database files if they do not exist
--crossdb Enable cross-database joins using the /_memory
database
--nolock Ignore locking, open locked files in read-only
mode
--ssl-keyfile TEXT SSL key file
--ssl-certfile TEXT SSL certificate file
--internal PATH Path to a persistent Datasette internal SQLite
database
--help Show this message and exit.
[[[end]]]",14,
118,datasette --get,"The --get option to datasette serve (or just datasette ) specifies the path to a page within Datasette and causes Datasette to output the content from that path without starting the web server.
This means that all of Datasette's functionality can be accessed directly from the command-line.
For example:
datasette --get '/-/versions.json' | jq .
{
""python"": {
""version"": ""3.8.5"",
""full"": ""3.8.5 (default, Jul 21 2020, 10:48:26) \n[Clang 11.0.3 (clang-1103.0.32.62)]""
},
""datasette"": {
""version"": ""0.46+15.g222a84a.dirty""
},
""asgi"": ""3.0"",
""uvicorn"": ""0.11.8"",
""sqlite"": {
""version"": ""3.32.3"",
""fts_versions"": [
""FTS5"",
""FTS4"",
""FTS3""
],
""extensions"": {
""json1"": null
},
""compile_options"": [
""COMPILER=clang-11.0.3"",
""ENABLE_COLUMN_METADATA"",
""ENABLE_FTS3"",
""ENABLE_FTS3_PARENTHESIS"",
""ENABLE_FTS4"",
""ENABLE_FTS5"",
""ENABLE_GEOPOLY"",
""ENABLE_JSON1"",
""ENABLE_PREUPDATE_HOOK"",
""ENABLE_RTREE"",
""ENABLE_SESSION"",
""MAX_VARIABLE_NUMBER=250000"",
""THREADSAFE=1""
]
}
}
You can use the --token TOKEN option to send an API token with the simulated request.
Or you can make a request as a specific actor by passing a JSON representation of that actor to --actor :
datasette --memory --actor '{""id"": ""root""}' --get '/-/actor.json'
The exit code of datasette --get will be 0 if the request succeeds and 1 if the request produced an HTTP status code other than 200 - e.g. a 404 or 500 error.
This lets you use datasette --get / to run tests against a Datasette application in a continuous integration environment such as GitHub Actions.",14,
119,datasette serve --help-settings,"This command outputs all of the available Datasette settings .
These can be passed to datasette serve using datasette serve --setting name value .
[[[cog
help([""--help-settings""])
]]]
Settings:
default_page_size Default page size for the table view
(default=100)
max_returned_rows Maximum rows that can be returned from a table or
custom query (default=1000)
max_insert_rows Maximum rows that can be inserted at a time using
the bulk insert API (default=100)
num_sql_threads Number of threads in the thread pool for
executing SQLite queries (default=3)
sql_time_limit_ms Time limit for a SQL query in milliseconds
(default=1000)
default_facet_size Number of values to return for requested facets
(default=30)
facet_time_limit_ms Time limit for calculating a requested facet
(default=200)
facet_suggest_time_limit_ms Time limit for calculating a suggested facet
(default=50)
allow_facet Allow users to specify columns to facet using
?_facet= parameter (default=True)
allow_download Allow users to download the original SQLite
database files (default=True)
allow_signed_tokens Allow users to create and use signed API tokens
(default=True)
default_allow_sql Allow anyone to run arbitrary SQL queries
(default=True)
max_signed_tokens_ttl Maximum allowed expiry time for signed API tokens
(default=0)
suggest_facets Calculate and display suggested facets
(default=True)
default_cache_ttl Default HTTP cache TTL (used in Cache-Control:
max-age= header) (default=5)
cache_size_kb SQLite cache size in KB (0 == use SQLite default)
(default=0)
allow_csv_stream Allow .csv?_stream=1 to download all rows
(ignoring max_returned_rows) (default=True)
max_csv_mb Maximum size allowed for CSV export in MB - set 0
to disable this limit (default=100)
truncate_cells_html Truncate cells longer than this in HTML table
view - set 0 to disable (default=2048)
force_https_urls Force URLs in API output to always use https://
protocol (default=False)
template_debug Allow display of template debug information with
?_context=1 (default=False)
trace_debug Allow display of SQL trace debug information with
?_trace=1 (default=False)
base_url Datasette URLs should use this base path
(default=/)
[[[end]]]",14,
120,datasette plugins,"Output JSON showing all currently installed plugins, their versions, whether they include static files or templates and which Plugin hooks they use.
[[[cog
help([""plugins"", ""--help""])
]]]
Usage: datasette plugins [OPTIONS]
List currently installed plugins
Options:
--all Include built-in default plugins
--requirements Output requirements.txt of installed plugins
--plugins-dir DIRECTORY Path to directory containing custom plugins
--help Show this message and exit.
[[[end]]]
Example output:
[
{
""name"": ""datasette-geojson"",
""static"": false,
""templates"": false,
""version"": ""0.3.1"",
""hooks"": [
""register_output_renderer""
]
},
{
""name"": ""datasette-geojson-map"",
""static"": true,
""templates"": false,
""version"": ""0.4.0"",
""hooks"": [
""extra_body_script"",
""extra_css_urls"",
""extra_js_urls""
]
},
{
""name"": ""datasette-leaflet"",
""static"": true,
""templates"": false,
""version"": ""0.2.2"",
""hooks"": [
""extra_body_script"",
""extra_template_vars""
]
}
]",14,
121,datasette install,"Install new Datasette plugins. This command works like pip install but ensures that your plugins will be installed into the same environment as Datasette.
This command:
datasette install datasette-cluster-map
Would install the datasette-cluster-map plugin.
[[[cog
help([""install"", ""--help""])
]]]
Usage: datasette install [OPTIONS] [PACKAGES]...
Install plugins and packages from PyPI into the same environment as Datasette
Options:
-U, --upgrade Upgrade packages to latest version
-r, --requirement PATH Install from requirements file
-e, --editable TEXT Install a project in editable mode from this path
--help Show this message and exit.
[[[end]]]",14,
122,datasette uninstall,"Uninstall one or more plugins.
[[[cog
help([""uninstall"", ""--help""])
]]]
Usage: datasette uninstall [OPTIONS] PACKAGES...
Uninstall plugins and Python packages from the Datasette environment
Options:
-y, --yes Don't ask for confirmation
--help Show this message and exit.
[[[end]]]",14,
123,datasette publish,"Shows a list of available deployment targets for publishing data with Datasette.
Additional deployment targets can be added by plugins that use the publish_subcommand(publish) hook.
[[[cog
help([""publish"", ""--help""])
]]]
Usage: datasette publish [OPTIONS] COMMAND [ARGS]...
Publish specified SQLite database files to the internet along with a
Datasette-powered interface and API
Options:
--help Show this message and exit.
Commands:
cloudrun Publish databases to Datasette running on Cloud Run
heroku Publish databases to Datasette running on Heroku
[[[end]]]",14,
124,datasette publish cloudrun,"See Publishing to Google Cloud Run .
[[[cog
help([""publish"", ""cloudrun"", ""--help""])
]]]
Usage: datasette publish cloudrun [OPTIONS] [FILES]...
Publish databases to Datasette running on Cloud Run
Options:
-m, --metadata FILENAME Path to JSON/YAML file containing metadata to
publish
--extra-options TEXT Extra options to pass to datasette serve
--branch TEXT Install datasette from a GitHub branch e.g.
main
--template-dir DIRECTORY Path to directory containing custom templates
--plugins-dir DIRECTORY Path to directory containing custom plugins
--static MOUNT:DIRECTORY Serve static files from this directory at
/MOUNT/...
--install TEXT Additional packages (e.g. plugins) to install
--plugin-secret ...
Secrets to pass to plugins, e.g. --plugin-
secret datasette-auth-github client_id xxx
--version-note TEXT Additional note to show on /-/versions
--secret TEXT Secret used for signing secure values, such as
signed cookies
--title TEXT Title for metadata
--license TEXT License label for metadata
--license_url TEXT License URL for metadata
--source TEXT Source label for metadata
--source_url TEXT Source URL for metadata
--about TEXT About label for metadata
--about_url TEXT About URL for metadata
-n, --name TEXT Application name to use when building
--service TEXT Cloud Run service to deploy (or over-write)
--spatialite Enable SpatialLite extension
--show-files Output the generated Dockerfile and
metadata.json
--memory TEXT Memory to allocate in Cloud Run, e.g. 1Gi
--cpu [1|2|4] Number of vCPUs to allocate in Cloud Run
--timeout INTEGER Build timeout in seconds
--apt-get-install TEXT Additional packages to apt-get install
--max-instances INTEGER Maximum Cloud Run instances
--min-instances INTEGER Minimum Cloud Run instances
--help Show this message and exit.
[[[end]]]",14,
125,datasette publish heroku,"See Publishing to Heroku .
[[[cog
help([""publish"", ""heroku"", ""--help""])
]]]
Usage: datasette publish heroku [OPTIONS] [FILES]...
Publish databases to Datasette running on Heroku
Options:
-m, --metadata FILENAME Path to JSON/YAML file containing metadata to
publish
--extra-options TEXT Extra options to pass to datasette serve
--branch TEXT Install datasette from a GitHub branch e.g.
main
--template-dir DIRECTORY Path to directory containing custom templates
--plugins-dir DIRECTORY Path to directory containing custom plugins
--static MOUNT:DIRECTORY Serve static files from this directory at
/MOUNT/...
--install TEXT Additional packages (e.g. plugins) to install
--plugin-secret ...
Secrets to pass to plugins, e.g. --plugin-
secret datasette-auth-github client_id xxx
--version-note TEXT Additional note to show on /-/versions
--secret TEXT Secret used for signing secure values, such as
signed cookies
--title TEXT Title for metadata
--license TEXT License label for metadata
--license_url TEXT License URL for metadata
--source TEXT Source label for metadata
--source_url TEXT Source URL for metadata
--about TEXT About label for metadata
--about_url TEXT About URL for metadata
-n, --name TEXT Application name to use when deploying
--tar TEXT --tar option to pass to Heroku, e.g.
--tar=/usr/local/bin/gtar
--generate-dir DIRECTORY Output generated application files and stop
without deploying
--help Show this message and exit.
[[[end]]]",14,
126,datasette package,"Package SQLite files into a Datasette Docker container, see datasette package .
[[[cog
help([""package"", ""--help""])
]]]
Usage: datasette package [OPTIONS] FILES...
Package SQLite files into a Datasette Docker container
Options:
-t, --tag TEXT Name for the resulting Docker container, can
optionally use name:tag format
-m, --metadata FILENAME Path to JSON/YAML file containing metadata to
publish
--extra-options TEXT Extra options to pass to datasette serve
--branch TEXT Install datasette from a GitHub branch e.g. main
--template-dir DIRECTORY Path to directory containing custom templates
--plugins-dir DIRECTORY Path to directory containing custom plugins
--static MOUNT:DIRECTORY Serve static files from this directory at /MOUNT/...
--install TEXT Additional packages (e.g. plugins) to install
--spatialite Enable SpatialLite extension
--version-note TEXT Additional note to show on /-/versions
--secret TEXT Secret used for signing secure values, such as
signed cookies
-p, --port INTEGER RANGE Port to run the server on, defaults to 8001
[1<=x<=65535]
--title TEXT Title for metadata
--license TEXT License label for metadata
--license_url TEXT License URL for metadata
--source TEXT Source label for metadata
--source_url TEXT Source URL for metadata
--about TEXT About label for metadata
--about_url TEXT About URL for metadata
--help Show this message and exit.
[[[end]]]",14,
127,datasette inspect,"Outputs JSON representing introspected data about one or more SQLite database files.
If you are opening an immutable database, you can pass this file to the --inspect-data option to improve Datasette's performance by allowing it to skip running row counts against the database when it first starts running:
datasette inspect mydatabase.db > inspect-data.json
datasette serve -i mydatabase.db --inspect-file inspect-data.json
This performance optimization is used automatically by some of the datasette publish commands. You are unlikely to need to apply this optimization manually.
[[[cog
help([""inspect"", ""--help""])
]]]
Usage: datasette inspect [OPTIONS] [FILES]...
Generate JSON summary of provided database files
This can then be passed to ""datasette --inspect-file"" to speed up count
operations against immutable database files.
Options:
--inspect-file TEXT
--load-extension PATH:ENTRYPOINT?
Path to a SQLite extension to load, and
optional entrypoint
--help Show this message and exit.
[[[end]]]",14,
128,datasette create-token,"Create a signed API token, see datasette create-token .
[[[cog
help([""create-token"", ""--help""])
]]]
Usage: datasette create-token [OPTIONS] ID
Create a signed API token for the specified actor ID
Example:
datasette create-token root --secret mysecret
To allow only ""view-database-download"" for all databases:
datasette create-token root --secret mysecret \
--all view-database-download
To allow ""create-table"" against a specific database:
datasette create-token root --secret mysecret \
--database mydb create-table
To allow ""insert-row"" against a specific table:
datasette create-token root --secret myscret \
--resource mydb mytable insert-row
Restricted actions can be specified multiple times using multiple --all,
--database, and --resource options.
Add --debug to see a decoded version of the token.
Options:
--secret TEXT Secret used for signing the API tokens
[required]
-e, --expires-after INTEGER Token should expire after this many seconds
-a, --all ACTION Restrict token to this action
-d, --database DB ACTION Restrict token to this action on this database
-r, --resource DB RESOURCE ACTION
Restrict token to this action on this database
resource (a table, SQL view or named query)
--debug Show decoded token
--plugins-dir DIRECTORY Path to directory containing custom plugins
--help Show this message and exit.
[[[end]]]",14,
129,Metadata,"Data loves metadata. Any time you run Datasette you can optionally include a
YAML or JSON file with metadata about your databases and tables. Datasette will then
display that information in the web UI.
Run Datasette like this:
datasette database1.db database2.db --metadata metadata.yaml
Your metadata.yaml file can look something like this:
[[[cog
from metadata_doc import metadata_example
metadata_example(cog, {
""title"": ""Custom title for your index page"",
""description"": ""Some description text can go here"",
""license"": ""ODbL"",
""license_url"": ""https://opendatacommons.org/licenses/odbl/"",
""source"": ""Original Data Source"",
""source_url"": ""http://example.com/""
})
]]]
[[[end]]]
Choosing YAML over JSON adds support for multi-line strings and comments.
The above metadata will be displayed on the index page of your Datasette-powered
site. The source and license information will also be included in the footer of
every page served by Datasette.
Any special HTML characters in description will be escaped. If you want to
include HTML in your description, you can use a description_html property
instead.",14,
130,Per-database and per-table metadata,"Metadata at the top level of the file will be shown on the index page and in the
footer on every page of the site. The license and source is expected to apply to
all of your data.
You can also provide metadata at the per-database or per-table level, like this:
[[[cog
metadata_example(cog, {
""databases"": {
""database1"": {
""source"": ""Alternative source"",
""source_url"": ""http://example.com/"",
""tables"": {
""example_table"": {
""description_html"": ""Custom table description"",
""license"": ""CC BY 3.0 US"",
""license_url"": ""https://creativecommons.org/licenses/by/3.0/us/""
}
}
}
}
})
]]]
[[[end]]]
Each of the top-level metadata fields can be used at the database and table level.",14,
131,"Source, license and about","The three visible metadata fields you can apply to everything, specific databases or specific tables are source, license and about. All three are optional.
source and source_url should be used to indicate where the underlying data came from.
license and license_url should be used to indicate the license under which the data can be used.
about and about_url can be used to link to further information about the project - an accompanying blog entry for example.
For each of these you can provide just the *_url field and Datasette will treat that as the default link label text and display the URL directly on the page.",14,
132,Column descriptions,"You can include descriptions for your columns by adding a ""columns"": {""name-of-column"": ""description-of-column""} block to your table metadata:
[[[cog
metadata_example(cog, {
""databases"": {
""database1"": {
""tables"": {
""example_table"": {
""columns"": {
""column1"": ""Description of column 1"",
""column2"": ""Description of column 2""
}
}
}
}
}
})
]]]
[[[end]]]
These will be displayed at the top of the table page, and will also show in the cog menu for each column.
You can see an example of how these look at latest.datasette.io/fixtures/roadside_attractions .",14,
133,Specifying units for a column,"Datasette supports attaching units to a column, which will be used when displaying
values from that column. SI prefixes will be used where appropriate.
Column units are configured in the metadata like so:
[[[cog
metadata_example(cog, {
""databases"": {
""database1"": {
""tables"": {
""example_table"": {
""units"": {
""column1"": ""metres"",
""column2"": ""Hz""
}
}
}
}
}
})
]]]
[[[end]]]
Units are interpreted using Pint , and you can see the full list of available units in
Pint's unit registry . You can also add custom units to the metadata, which will be
registered with Pint:
[[[cog
metadata_example(cog, {
""custom_units"": [
""decibel = [] = dB""
]
})
]]]
[[[end]]]",14,
134,Setting a default sort order,"By default Datasette tables are sorted by primary key. You can over-ride this default for a specific table using the ""sort"" or ""sort_desc"" metadata properties:
[[[cog
metadata_example(cog, {
""databases"": {
""mydatabase"": {
""tables"": {
""example_table"": {
""sort"": ""created""
}
}
}
}
})
]]]
[[[end]]]
Or use ""sort_desc"" to sort in descending order:
[[[cog
metadata_example(cog, {
""databases"": {
""mydatabase"": {
""tables"": {
""example_table"": {
""sort_desc"": ""created""
}
}
}
}
})
]]]
[[[end]]]",14,
135,Setting a custom page size,"Datasette defaults to displaying 100 rows per page, for both tables and views. You can change this default page size on a per-table or per-view basis using the ""size"" key in metadata.json :
[[[cog
metadata_example(cog, {
""databases"": {
""mydatabase"": {
""tables"": {
""example_table"": {
""size"": 10
}
}
}
}
})
]]]
[[[end]]]
This size can still be over-ridden by passing e.g. ?_size=50 in the query string.",14,
136,Setting which columns can be used for sorting,"Datasette allows any column to be used for sorting by default. If you need to
control which columns are available for sorting you can do so using the optional
sortable_columns key:
[[[cog
metadata_example(cog, {
""databases"": {
""database1"": {
""tables"": {
""example_table"": {
""sortable_columns"": [
""height"",
""weight""
]
}
}
}
}
})
]]]
[[[end]]]
This will restrict sorting of example_table to just the height and
weight columns.
You can also disable sorting entirely by setting ""sortable_columns"": []
You can use sortable_columns to enable specific sort orders for a view called name_of_view in the database my_database like so:
[[[cog
metadata_example(cog, {
""databases"": {
""my_database"": {
""tables"": {
""name_of_view"": {
""sortable_columns"": [
""clicks"",
""impressions""
]
}
}
}
}
})
]]]
[[[end]]]",14,
137,Specifying the label column for a table,"Datasette's HTML interface attempts to display foreign key references as
labelled hyperlinks. By default, it looks for referenced tables that only have
two columns: a primary key column and one other. It assumes that the second
column should be used as the link label.
If your table has more than two columns you can specify which column should be
used for the link label with the label_column property:
[[[cog
metadata_example(cog, {
""databases"": {
""database1"": {
""tables"": {
""example_table"": {
""label_column"": ""title""
}
}
}
}
})
]]]
[[[end]]]",14,
138,Hiding tables,"You can hide tables from the database listing view (in the same way that FTS and
SpatiaLite tables are automatically hidden) using ""hidden"": true :
[[[cog
metadata_example(cog, {
""databases"": {
""database1"": {
""tables"": {
""example_table"": {
""hidden"": True
}
}
}
}
})
]]]
[[[end]]]",14,
139,Metadata reference,A full reference of every supported option in a metadata.json or metadata.yaml file.,14,
140,Top-level metadata,"""Top-level"" metadata refers to fields that can be specified at the root level of a metadata file. These attributes are meant to describe the entire Datasette instance.
The following are the full list of allowed top-level metadata fields:
title
description
description_html
license
license_url
source
source_url",14,
141,Database-level metadata,"""Database-level"" metadata refers to fields that can be specified for each database in a Datasette instance. These attributes should be listed under a database inside the ""databases"" field.
The following are the full list of allowed database-level metadata fields:
source
source_url
license
license_url
about
about_url",14,
142,Table-level metadata,"""Table-level"" metadata refers to fields that can be specified for each table in a Datasette instance. These attributes should be listed under a specific table using the ""tables"" field.
The following are the full list of allowed table-level metadata fields:
source
source_url
license
license_url
about
about_url
hidden
sort/sort_desc
size
sortable_columns
label_column
facets
fts_table
fts_pk
searchmode
columns",14,
143,Running SQL queries,"Datasette treats SQLite database files as read-only and immutable. This means it is not possible to execute INSERT or UPDATE statements using Datasette, which allows us to expose SELECT statements to the outside world without needing to worry about SQL injection attacks.
The easiest way to execute custom SQL against Datasette is through the web UI. The database index page includes a SQL editor that lets you run any SELECT query you like. You can also construct queries using the filter interface on the tables page, then click ""View and edit SQL"" to open that query in the custom SQL editor.
Note that this interface is only available if the execute-sql permission is allowed. See Controlling the ability to execute arbitrary SQL .
Any Datasette SQL query is reflected in the URL of the page, allowing you to bookmark them, share them with others and navigate through previous queries using your browser back button.
You can also retrieve the results of any query as JSON by adding .json to the base URL.",14,
144,Named parameters,"Datasette has special support for SQLite named parameters. Consider a SQL query like this:
select * from Street_Tree_List
where ""PermitNotes"" like :notes
and ""qSpecies"" = :species
If you execute this query using the custom query editor, Datasette will extract the two named parameters and use them to construct form fields for you to provide values.
You can also provide values for these fields by constructing a URL:
/mydatabase?sql=select...&species=44
SQLite string escaping rules will be applied to values passed using named parameters - they will be wrapped in quotes and their content will be correctly escaped.
Values from named parameters are treated as SQLite strings. If you need to perform numeric comparisons on them you should cast them to an integer or float first using cast(:name as integer) or cast(:name as real) , for example:
select * from Street_Tree_List
where latitude > cast(:min_latitude as real)
and latitude < cast(:max_latitude as real)
Datasette disallows custom SQL queries containing the string PRAGMA (with a small number of exceptions ) as SQLite pragma statements can be used to change database settings at runtime. If you need to include the string ""pragma"" in a query you can do so safely using a named parameter.",14,
145,Views,"If you want to bundle some pre-written SQL queries with your Datasette-hosted database you can do so in two ways. The first is to include SQL views in your database - Datasette will then list those views on your database index page.
The quickest way to create views is with the SQLite command-line interface:
sqlite3 sf-trees.db
SQLite version 3.19.3 2017-06-27 16:48:08
Enter "".help"" for usage hints.
sqlite> CREATE VIEW demo_view AS select qSpecies from Street_Tree_List;
You can also use the sqlite-utils tool to create a view :
sqlite-utils create-view sf-trees.db demo_view ""select qSpecies from Street_Tree_List""",14,
146,Canned queries,"As an alternative to adding views to your database, you can define canned queries inside your datasette.yaml file. Here's an example:
[[[cog
from metadata_doc import config_example, config_example
config_example(cog, {
""databases"": {
""sf-trees"": {
""queries"": {
""just_species"": {
""sql"": ""select qSpecies from Street_Tree_List""
}
}
}
}
})
]]]
[[[end]]]
Then run Datasette like this:
datasette sf-trees.db -m metadata.json
Each canned query will be listed on the database index page, and will also get its own URL at:
/database-name/canned-query-name
For the above example, that URL would be:
/sf-trees/just_species
You can optionally include ""title"" and ""description"" keys to show a title and description on the canned query page. As with regular table metadata you can alternatively specify ""description_html"" to have your description rendered as HTML (rather than having HTML special characters escaped).",14,
147,Canned query parameters,"Canned queries support named parameters, so if you include those in the SQL you will then be able to enter them using the form fields on the canned query page or by adding them to the URL. This means canned queries can be used to create custom JSON APIs based on a carefully designed SQL statement.
Here's an example of a canned query with a named parameter:
select neighborhood, facet_cities.name, state
from facetable
join facet_cities on facetable.city_id = facet_cities.id
where neighborhood like '%' || :text || '%'
order by neighborhood;
In the canned query configuration looks like this:
[[[cog
config_example(cog, """"""
databases:
fixtures:
queries:
neighborhood_search:
title: Search neighborhoods
sql: |-
select neighborhood, facet_cities.name, state
from facetable
join facet_cities on facetable.city_id = facet_cities.id
where neighborhood like '%' || :text || '%'
order by neighborhood
"""""")
]]]
[[[end]]]
Note that we are using SQLite string concatenation here - the || operator - to add wildcard % characters to the string provided by the user.
You can try this canned query out here:
https://latest.datasette.io/fixtures/neighborhood_search?text=town
In this example the :text named parameter is automatically extracted from the query using a regular expression.
You can alternatively provide an explicit list of named parameters using the ""params"" key, like this:
[[[cog
config_example(cog, """"""
databases:
fixtures:
queries:
neighborhood_search:
title: Search neighborhoods
params:
- text
sql: |-
select neighborhood, facet_cities.name, state
from facetable
join facet_cities on facetable.city_id = facet_cities.id
where neighborhood like '%' || :text || '%'
order by neighborhood
"""""")
]]]
[[[end]]]",14,
148,Additional canned query options,Additional options can be specified for canned queries in the YAML or JSON configuration.,14,
149,hide_sql,"Canned queries default to displaying their SQL query at the top of the page. If the query is extremely long you may want to hide it by default, with a ""show"" link that can be used to make it visible.
Add the ""hide_sql"": true option to hide the SQL query by default.",14,
150,fragment,"Some plugins, such as datasette-vega , can be configured by including additional data in the fragment hash of the URL - the bit that comes after a # symbol.
You can set a default fragment hash that will be included in the link to the canned query from the database index page using the ""fragment"" key.
This example demonstrates both fragment and hide_sql :
[[[cog
config_example(cog, """"""
databases:
fixtures:
queries:
neighborhood_search:
fragment: fragment-goes-here
hide_sql: true
sql: |-
select neighborhood, facet_cities.name, state
from facetable join facet_cities on facetable.city_id = facet_cities.id
where neighborhood like '%' || :text || '%' order by neighborhood;
"""""")
]]]
[[[end]]]
See here for a demo of this in action.",14,
151,Writable canned queries,"Canned queries by default are read-only. You can use the ""write"": true key to indicate that a canned query can write to the database.
See Access to specific canned queries for details on how to add permission checks to canned queries, using the ""allow"" key.
[[[cog
config_example(cog, {
""databases"": {
""mydatabase"": {
""queries"": {
""add_name"": {
""sql"": ""INSERT INTO names (name) VALUES (:name)"",
""write"": True
}
}
}
}
})
]]]
[[[end]]]
This configuration will create a page at /mydatabase/add_name displaying a form with a name field. Submitting that form will execute the configured INSERT query.
You can customize how Datasette represents success and errors using the following optional properties:
on_success_message - the message shown when a query is successful
on_success_message_sql - alternative to on_success_message : a SQL query that should be executed to generate the message
on_success_redirect - the path or URL the user is redirected to on success
on_error_message - the message shown when a query throws an error
on_error_redirect - the path or URL the user is redirected to on error
For example:
[[[cog
config_example(cog, {
""databases"": {
""mydatabase"": {
""queries"": {
""add_name"": {
""sql"": ""INSERT INTO names (name) VALUES (:name)"",
""params"": [""name""],
""write"": True,
""on_success_message_sql"": ""select 'Name inserted: ' || :name"",
""on_success_redirect"": ""/mydatabase/names"",
""on_error_message"": ""Name insert failed"",
""on_error_redirect"": ""/mydatabase"",
}
}
}
}
})
]]]
[[[end]]]
You can use ""params"" to explicitly list the named parameters that should be displayed as form fields - otherwise they will be automatically detected. ""params"" is not necessary in the above example, since without it ""name"" would be automatically detected from the query.
You can pre-populate form fields when the page first loads using a query string, e.g. /mydatabase/add_name?name=Prepopulated . The user will have to submit the form to execute the query.
If you specify a query in ""on_success_message_sql"" , that query will be executed after the main query. The first column of the first row return by that query will be displayed as a success message. Named parameters from the main query will be made available to the success message query as well.",14,
152,Magic parameters,"Named parameters that start with an underscore are special: they can be used to automatically add values created by Datasette that are not contained in the incoming form fields or query string.
These magic parameters are only supported for canned queries: to avoid security issues (such as queries that extract the user's private cookies) they are not available to SQL that is executed by the user as a custom SQL query.
Available magic parameters are:
_actor_* - e.g. _actor_id , _actor_name
Fields from the currently authenticated Actors .
_header_* - e.g. _header_user_agent
Header from the incoming HTTP request. The key should be in lower case and with hyphens converted to underscores e.g. _header_user_agent or _header_accept_language .
_cookie_* - e.g. _cookie_lang
The value of the incoming cookie of that name.
_now_epoch
The number of seconds since the Unix epoch.
_now_date_utc
The date in UTC, e.g. 2020-06-01
_now_datetime_utc
The ISO 8601 datetime in UTC, e.g. 2020-06-24T18:01:07Z
_random_chars_* - e.g. _random_chars_128
A random string of characters of the specified length.
Here's an example configuration that adds a message from the authenticated user, storing various pieces of additional metadata using magic parameters:
[[[cog
config_example(cog, """"""
databases:
mydatabase:
queries:
add_message:
allow:
id: ""*""
sql: |-
INSERT INTO messages (
user_id, message, datetime
) VALUES (
:_actor_id, :message, :_now_datetime_utc
)
write: true
"""""")
]]]
[[[end]]]
The form presented at /mydatabase/add_message will have just a field for message - the other parameters will be populated by the magic parameter mechanism.
Additional custom magic parameters can be added by plugins using the register_magic_parameters(datasette) hook.",14,
153,JSON API for writable canned queries,"Writable canned queries can also be accessed using a JSON API. You can POST data to them using JSON, and you can request that their response is returned to you as JSON.
To submit JSON to a writable canned query, encode key/value parameters as a JSON document:
POST /mydatabase/add_message
{""message"": ""Message goes here""}
You can also continue to submit data using regular form encoding, like so:
POST /mydatabase/add_message
message=Message+goes+here
There are three options for specifying that you would like the response to your request to return JSON data, as opposed to an HTTP redirect to another page.
Set an Accept: application/json header on your request
Include ?_json=1 in the URL that you POST to
Include ""_json"": 1 in your JSON body, or &_json=1 in your form encoded body
The JSON response will look like this:
{
""ok"": true,
""message"": ""Query executed, 1 row affected"",
""redirect"": ""/data/add_name""
}
The ""message"" and ""redirect"" values here will take into account on_success_message , on_success_message_sql , on_success_redirect , on_error_message and on_error_redirect , if they have been set.",14,
154,Pagination,"Datasette's default table pagination is designed to be extremely efficient. SQL OFFSET/LIMIT pagination can have a significant performance penalty once you get into multiple thousands of rows, as each page still requires the database to scan through every preceding row to find the correct offset.
When paginating through tables, Datasette instead orders the rows in the table by their primary key and performs a WHERE clause against the last seen primary key for the previous page. For example:
select rowid, * from Tree_List where rowid > 200 order by rowid limit 101
This represents page three for this particular table, with a page size of 100.
Note that we request 101 items in the limit clause rather than 100. This allows us to detect if we are on the last page of the results: if the query returns less than 101 rows we know we have reached the end of the pagination set. Datasette will only return the first 100 rows - the 101st is used purely to detect if there should be another page.
Since the where clause acts against the index on the primary key, the query is extremely fast even for records that are a long way into the overall pagination set.",14,
155,Cross-database queries,"SQLite has the ability to run queries that join across multiple databases. Up to ten databases can be attached to a single SQLite connection and queried together.
Datasette can execute joins across multiple databases if it is started with the --crossdb option:
datasette fixtures.db extra_database.db --crossdb
If it is started in this way, the /_memory page can be used to execute queries that join across multiple databases.
References to tables in attached databases should be preceded by the database name and a period.
For example, this query will show a list of tables across both of the above databases:
select
'fixtures' as database, *
from
[fixtures].sqlite_master
union
select
'extra_database' as database, *
from
[extra_database].sqlite_master
Try that out here .",14,
156,JavaScript plugins,"Datasette can run custom JavaScript in several different ways:
Datasette plugins written in Python can use the extra_js_urls() or extra_body_script() plugin hooks to inject JavaScript into a page
Datasette instances with custom templates can include additional JavaScript in those templates
The extra_js_urls key in datasette.yaml can be used to include extra JavaScript
There are no limitations on what this JavaScript can do. It is executed directly by the browser, so it can manipulate the DOM, fetch additional data and do anything else that JavaScript is capable of.
Custom JavaScript has security implications, especially for authenticated Datasette instances where the JavaScript might run in the context of the authenticated user. It's important to carefully review any JavaScript you run in your Datasette instance.",14,
157,The datasette_init event,"Datasette emits a custom event called datasette_init when the page is loaded. This event is dispatched on the document object, and includes a detail object with a reference to the datasetteManager object.
Your JavaScript code can listen out for this event using document.addEventListener() like this:
document.addEventListener(""datasette_init"", function (evt) {
const manager = evt.detail;
console.log(""Datasette version:"", manager.VERSION);
});",14,
158,datasetteManager,"The datasetteManager object
VERSION - string
The version of Datasette
plugins - Map()
A Map of currently loaded plugin names to plugin implementations
registerPlugin(name, implementation)
Call this to register a plugin, passing its name and implementation
selectors - object
An object providing named aliases to useful CSS selectors, listed below",14,
159,JavaScript plugin objects,"JavaScript plugins are blocks of code that can be registered with Datasette using the registerPlugin() method on the datasetteManager object.
The implementation object passed to this method should include a version key defining the plugin version, and one or more of the following named functions providing the implementation of the plugin:",14,
160,makeAboveTablePanelConfigs(),"This method should return a JavaScript array of objects defining additional panels to be added to the top of the table page. Each object should have the following:
id - string
A unique string ID for the panel, for example map-panel
label - string
A human-readable label for the panel
render(node) - function
A function that will be called with a DOM node to render the panel into
This example shows how a plugin might define a single panel:
document.addEventListener('datasette_init', function(ev) {
ev.detail.registerPlugin('panel-plugin', {
version: 0.1,
makeAboveTablePanelConfigs: () => {
return [
{
id: 'first-panel',
label: 'First panel',
render: node => {
node.innerHTML = '
My custom panel
This is a custom panel that I added using a JavaScript plugin
';
}
}
]
}
});
});
When a page with a table loads, all registered plugins that implement makeAboveTablePanelConfigs() will be called and panels they return will be added to the top of the table page.",14,
161,makeColumnActions(columnDetails),"This method, if present, will be called when Datasette is rendering the cog action menu icons that appear at the top of the table view. By default these include options like ""Sort ascending/descending"" and ""Facet by this"", but plugins can return additional actions to be included in this menu.
The method will be called with a columnDetails object with the following keys:
columnName - string
The name of the column
columnNotNull - boolean
True if the column is defined as NOT NULL
columnType - string
The SQLite data type of the column
isPk - boolean
True if the column is part of the primary key
It should return a JavaScript array of objects each with a label and onClick property:
label - string
The human-readable label for the action
onClick(evt) - function
A function that will be called when the action is clicked
The evt object passed to the onClick is the standard browser event object that triggered the click.
This example plugin adds two menu items - one to copy the column name to the clipboard and another that displays the column metadata in an alert() window:
document.addEventListener('datasette_init', function(ev) {
ev.detail.registerPlugin('column-name-plugin', {
version: 0.1,
makeColumnActions: (columnDetails) => {
return [
{
label: 'Copy column to clipboard',
onClick: async (evt) => {
await navigator.clipboard.writeText(columnDetails.columnName)
}
},
{
label: 'Alert column metadata',
onClick: () => alert(JSON.stringify(columnDetails, null, 2))
}
];
}
});
});",14,
162,Selectors,"These are available on the selectors property of the datasetteManager object.
const DOM_SELECTORS = {
/** Should have one match */
jsonExportLink: "".export-links a[href*=json]"",
/** Event listeners that go outside of the main table, e.g. existing scroll listener */
tableWrapper: "".table-wrapper"",
table: ""table.rows-and-columns"",
aboveTablePanel: "".above-table-panel"",
// These could have multiple matches
/** Used for selecting table headers. Use makeColumnActions if you want to add menu items. */
tableHeaders: `table.rows-and-columns th`,
/** Used to add ""where"" clauses to query using direct manipulation */
filterRows: "".filter-row"",
/** Used to show top available enum values for a column (""facets"") */
facetResults: "".facet-results [data-column]"",
};",14,
163,Custom pages and templates,Datasette provides a number of ways of customizing the way data is displayed.,14,
164,CSS classes on the ,"Every default template includes CSS classes in the body designed to support
custom styling.
The index template (the top level page at / ) gets this:
The database template ( /dbname ) gets this:
The custom SQL template ( /dbname?sql=... ) gets this:
A canned query template ( /dbname/queryname ) gets this:
The table template ( /dbname/tablename ) gets:
The row template ( /dbname/tablename/rowid ) gets:
The db-x and table-x classes use the database or table names themselves if
they are valid CSS identifiers. If they aren't, we strip any invalid
characters out and append a 6 character md5 digest of the original name, in
order to ensure that multiple tables which resolve to the same stripped
character version still have different CSS classes.
Some examples:
""simple"" => ""simple""
""MixedCase"" => ""MixedCase""
""-no-leading-hyphens"" => ""no-leading-hyphens-65bea6""
""_no-leading-underscores"" => ""no-leading-underscores-b921bc""
""no spaces"" => ""no-spaces-7088d7""
""-"" => ""336d5e""
""no $ characters"" => ""no--characters-59e024""
and
elements also get custom CSS classes reflecting the
database column they are representing, for example:
",14,
165,Serving static files,"Datasette can serve static files for you, using the --static option.
Consider the following directory structure:
metadata.json
static-files/styles.css
static-files/app.js
You can start Datasette using --static assets:static-files/ to serve those
files from the /assets/ mount point:
datasette --config datasette.yaml --static assets:static-files/ --memory
The following URLs will now serve the content from those CSS and JS files:
http://localhost:8001/assets/styles.css
http://localhost:8001/assets/app.js
You can reference those files from datasette.yaml like this, see custom CSS and JavaScript for more details:
[[[cog
from metadata_doc import config_example
config_example(cog, """"""
extra_css_urls:
- /assets/styles.css
extra_js_urls:
- /assets/app.js
"""""")
]]]
[[[end]]]",14,
166,Publishing static assets,"The datasette publish command can be used to publish your static assets,
using the same syntax as above:
datasette publish cloudrun mydb.db --static assets:static-files/
This will upload the contents of the static-files/ directory as part of the
deployment, and configure Datasette to correctly serve the assets from /assets/ .",14,
167,Custom templates,"By default, Datasette uses default templates that ship with the package.
You can over-ride these templates by specifying a custom --template-dir like
this:
datasette mydb.db --template-dir=mytemplates/
Datasette will now first look for templates in that directory, and fall back on
the defaults if no matches are found.
It is also possible to over-ride templates on a per-database, per-row or per-
table basis.
The lookup rules Datasette uses are as follows:
Index page (/):
index.html
Database page (/mydatabase):
database-mydatabase.html
database.html
Custom query page (/mydatabase?sql=...):
query-mydatabase.html
query.html
Canned query page (/mydatabase/canned-query):
query-mydatabase-canned-query.html
query-mydatabase.html
query.html
Table page (/mydatabase/mytable):
table-mydatabase-mytable.html
table.html
Row page (/mydatabase/mytable/id):
row-mydatabase-mytable.html
row.html
Table of rows and columns include on table page:
_table-table-mydatabase-mytable.html
_table-mydatabase-mytable.html
_table.html
Table of rows and columns include on row page:
_table-row-mydatabase-mytable.html
_table-mydatabase-mytable.html
_table.html
If a table name has spaces or other unexpected characters in it, the template
filename will follow the same rules as our custom CSS classes - for
example, a table called ""Food Trucks"" will attempt to load the following
templates:
table-mydatabase-Food-Trucks-399138.html
table.html
You can find out which templates were considered for a specific page by viewing
source on that page and looking for an HTML comment at the bottom. The comment
will look something like this:
This example is from the canned query page for a query called ""tz"" in the
database called ""mydb"". The asterisk shows which template was selected - so in
this case, Datasette found a template file called query-mydb-tz.html and
used that - but if that template had not been found, it would have tried for
query-mydb.html or the default query.html .
It is possible to extend the default templates using Jinja template
inheritance. If you want to customize EVERY row template with some additional
content you can do so by creating a row.html template like this:
{% extends ""default:row.html"" %}
{% block content %}
EXTRA HTML AT THE TOP OF THE CONTENT BLOCK
This line renders the original block:
{{ super() }}
{% endblock %}
Note the default:row.html template name, which ensures Jinja will inherit
from the default template.
The _table.html template is included by both the row and the table pages,
and a list of rows. The default _table.html template renders them as an
HTML template and can be seen here .
You can provide a custom template that applies to all of your databases and
tables, or you can provide custom templates for specific tables using the
template naming scheme described above.
If you want to present your data in a format other than an HTML table, you
can do so by looping through display_rows in your own _table.html
template. You can use {{ row[""column_name""] }} to output the raw value
of a specific column.
If you want to output the rendered HTML version of a column, including any
links to foreign keys, you can use {{ row.display(""column_name"") }} .
Here is an example of a custom _table.html template:
{% for row in display_rows %}
{{ row[""title""] }}
{{ row[""description""] }}
Category: {{ row.display(""category_id"") }}
{% endfor %}",14,
168,Custom pages,"You can add templated pages to your Datasette instance by creating HTML files in a pages directory within your templates directory.
For example, to add a custom page that is served at http://localhost/about you would create a file in templates/pages/about.html , then start Datasette like this:
datasette mydb.db --template-dir=templates/
You can nest directories within pages to create a nested structure. To create a http://localhost:8001/about/map page you would create templates/pages/about/map.html .",14,
169,Path parameters for pages,"You can define custom pages that match multiple paths by creating files with {variable} definitions in their filenames.
For example, to capture any request to a URL matching /about/* , you would create a template in the following location:
templates/pages/about/{slug}.html
A hit to /about/news would render that template and pass in a variable called slug with a value of ""news"" .
If you use this mechanism don't forget to return a 404 if the referenced content could not be found. You can do this using {{ raise_404() }} described below.
Templates defined using custom page routes work particularly well with the sql() template function from datasette-template-sql or the graphql() template function from datasette-graphql .",14,
170,Custom headers and status codes,"Custom pages default to being served with a content-type of text/html; charset=utf-8 and a 200 status code. You can change these by calling a custom function from within your template.
For example, to serve a custom page with a 418 I'm a teapot HTTP status code, create a file in pages/teapot.html containing the following:
{{ custom_status(418) }}
Teapot
I'm a teapot
To serve a custom HTTP header, add a custom_header(name, value) function call. For example:
{{ custom_status(418) }}
{{ custom_header(""x-teapot"", ""I am"") }}
Teapot
I'm a teapot
You can verify this is working using curl like this:
curl -I 'http://127.0.0.1:8001/teapot'
HTTP/1.1 418
date: Sun, 26 Apr 2020 18:38:30 GMT
server: uvicorn
x-teapot: I am
content-type: text/html; charset=utf-8",14,
171,Returning 404s,"To indicate that content could not be found and display the default 404 page you can use the raise_404(message) function:
{% if not rows %}
{{ raise_404(""Content not found"") }}
{% endif %}
If you call raise_404() the other content in your template will be ignored.",14,
172,Custom redirects,"You can use the custom_redirect(location) function to redirect users to another page, for example in a file called pages/datasette.html :
{{ custom_redirect(""https://github.com/simonw/datasette"") }}
Now requests to http://localhost:8001/datasette will result in a redirect.
These redirects are served with a 302 Found status code by default. You can send a 301 Moved Permanently code by passing 301 as the second argument to the function:
{{ custom_redirect(""https://github.com/simonw/datasette"", 301) }}",14,
173,Custom error pages,"Datasette returns an error page if an unexpected error occurs, access is forbidden or content cannot be found.
You can customize the response returned for these errors by providing a custom error page template.
Content not found errors use a 404.html template. Access denied errors use 403.html . Invalid input errors use 400.html . Unexpected errors of other kinds use 500.html .
If a template for the specific error code is not found a template called error.html will be used instead. If you do not provide that template Datasette's default error.html template will be used.
The error template will be passed the following context:
status - integer
The integer HTTP status code, e.g. 404, 500, 403, 400.
error - string
Details of the specific error, usually a full sentence.
title - string or None
A title for the page representing the class of error. This is often None for errors that do not provide a title separate from their error message.",14,
174,Writing plugins,"You can write one-off plugins that apply to just one Datasette instance, or you can write plugins which can be installed using pip and can be shipped to the Python Package Index ( PyPI ) for other people to install.
Want to start by looking at an example? The Datasette plugins directory lists more than 90 open source plugins with code you can explore. The plugin hooks page includes links to example plugins for each of the documented hooks.",14,
175,Tracing plugin hooks,"The DATASETTE_TRACE_PLUGINS environment variable turns on detailed tracing showing exactly which hooks are being run. This can be useful for understanding how Datasette is using your plugin.
DATASETTE_TRACE_PLUGINS=1 datasette mydb.db
Example output:
actor_from_request:
{ 'datasette': ,
'request': }
Hook implementations:
[ >,
>,
>]
Results:
[{'id': 'root'}]",14,
176,Writing one-off plugins,"The quickest way to start writing a plugin is to create a my_plugin.py file and drop it into your plugins/ directory. Here is an example plugin, which adds a new custom SQL function called hello_world() which takes no arguments and returns the string Hello world! .
from datasette import hookimpl
@hookimpl
def prepare_connection(conn):
conn.create_function(
""hello_world"", 0, lambda: ""Hello world!""
)
If you save this in plugins/my_plugin.py you can then start Datasette like this:
datasette serve mydb.db --plugins-dir=plugins/
Now you can navigate to http://localhost:8001/mydb and run this SQL:
select hello_world();
To see the output of your plugin.",14,
177,Starting an installable plugin using cookiecutter,"Plugins that can be installed should be written as Python packages using a setup.py file.
The quickest way to start writing one an installable plugin is to use the datasette-plugin cookiecutter template. This creates a new plugin structure for you complete with an example test and GitHub Actions workflows for testing and publishing your plugin.
Install cookiecutter and then run this command to start building a plugin using the template:
cookiecutter gh:simonw/datasette-plugin
Read a cookiecutter template for writing Datasette plugins for more information about this template.",14,
178,Packaging a plugin,"Plugins can be packaged using Python setuptools. You can see an example of a packaged plugin at https://github.com/simonw/datasette-plugin-demos
The example consists of two files: a setup.py file that defines the plugin:
from setuptools import setup
VERSION = ""0.1""
setup(
name=""datasette-plugin-demos"",
description=""Examples of plugins for Datasette"",
author=""Simon Willison"",
url=""https://github.com/simonw/datasette-plugin-demos"",
license=""Apache License, Version 2.0"",
version=VERSION,
py_modules=[""datasette_plugin_demos""],
entry_points={
""datasette"": [
""plugin_demos = datasette_plugin_demos""
]
},
install_requires=[""datasette""],
)
And a Python module file, datasette_plugin_demos.py , that implements the plugin:
from datasette import hookimpl
import random
@hookimpl
def prepare_jinja2_environment(env):
env.filters[""uppercase""] = lambda u: u.upper()
@hookimpl
def prepare_connection(conn):
conn.create_function(
""random_integer"", 2, random.randint
)
Having built a plugin in this way you can turn it into an installable package using the following command:
python3 setup.py sdist
This will create a .tar.gz file in the dist/ directory.
You can then install your new plugin into a Datasette virtual environment or Docker container using pip :
pip install datasette-plugin-demos-0.1.tar.gz
To learn how to upload your plugin to PyPI for use by other people, read the PyPA guide to Packaging and distributing projects .",14,
179,Static assets,"If your plugin has a static/ directory, Datasette will automatically configure itself to serve those static assets from the following path:
/-/static-plugins/NAME_OF_PLUGIN_PACKAGE/yourfile.js
Use the datasette.urls.static_plugins(plugin_name, path) method to generate URLs to that asset that take the base_url setting into account, see datasette.urls .
To bundle the static assets for a plugin in the package that you publish to PyPI, add the following to the plugin's setup.py :
package_data = (
{
""datasette_plugin_name"": [
""static/plugin.js"",
],
},
)
Where datasette_plugin_name is the name of the plugin package (note that it uses underscores, not hyphens) and static/plugin.js is the path within that package to the static file.
datasette-cluster-map is a useful example of a plugin that includes packaged static assets in this way.",14,
180,Custom templates,"If your plugin has a templates/ directory, Datasette will attempt to load templates from that directory before it uses its own default templates.
The priority order for template loading is:
templates from the --template-dir argument, if specified
templates from the templates/ directory in any installed plugins
default templates that ship with Datasette
See Custom pages and templates for more details on how to write custom templates, including which filenames to use to customize which parts of the Datasette UI.
Templates should be bundled for distribution using the same package_data mechanism in setup.py described for static assets above, for example:
package_data = (
{
""datasette_plugin_name"": [
""templates/my_template.html"",
],
},
)
You can also use wildcards here such as templates/*.html . See datasette-edit-schema for an example of this pattern.",14,
181,Writing plugins that accept configuration,"When you are writing plugins, you can access plugin configuration like this using the datasette plugin_config() method. If you know you need plugin configuration for a specific table, you can access it like this:
plugin_config = datasette.plugin_config(
""datasette-cluster-map"", database=""sf-trees"", table=""Street_Tree_List""
)
This will return the {""latitude_column"": ""lat"", ""longitude_column"": ""lng""} in the above example.
If there is no configuration for that plugin, the method will return None .
If it cannot find the requested configuration at the table layer, it will fall back to the database layer and then the root layer. For example, a user may have set the plugin configuration option inside datasette.yaml like so:
[[[cog
from metadata_doc import metadata_example
metadata_example(cog, {
""databases"": {
""sf-trees"": {
""plugins"": {
""datasette-cluster-map"": {
""latitude_column"": ""xlat"",
""longitude_column"": ""xlng""
}
}
}
}
})
]]]
[[[end]]]
In this case, the above code would return that configuration for ANY table within the sf-trees database.
The plugin configuration could also be set at the top level of datasette.yaml :
[[[cog
metadata_example(cog, {
""plugins"": {
""datasette-cluster-map"": {
""latitude_column"": ""xlat"",
""longitude_column"": ""xlng""
}
}
})
]]]
[[[end]]]
Now that datasette-cluster-map plugin configuration will apply to every table in every database.",14,
182,Designing URLs for your plugin,"You can register new URL routes within Datasette using the register_routes(datasette) plugin hook.
Datasette's default URLs include these:
/dbname - database page
/dbname/tablename - table page
/dbname/tablename/pk - row page
See Pages and API endpoints and Introspection for more default URL routes.
To avoid accidentally conflicting with a database file that may be loaded into Datasette, plugins should register URLs using a /-/ prefix. For example, if your plugin adds a new interface for uploading Excel files you might register a URL route like this one:
/-/upload-excel
Try to avoid registering URLs that clash with other plugins that your users might have installed. There is no central repository of reserved URL paths (yet) but you can review existing plugins by browsing the plugins directory .
If your plugin includes functionality that relates to a specific database you could also register a URL route like this:
/dbname/-/upload-excel
Or for a specific table like this:
/dbname/tablename/-/modify-table-schema
Note that a row could have a primary key of - and this URL scheme will still work, because Datasette row pages do not ever have a trailing slash followed by additional path components.",14,
183,Building URLs within plugins,"Plugins that define their own custom user interface elements may need to link to other pages within Datasette.
This can be a bit tricky if the Datasette instance is using the base_url configuration setting to run behind a proxy, since that can cause Datasette's URLs to include an additional prefix.
The datasette.urls object provides internal methods for correctly generating URLs to different pages within Datasette, taking any base_url configuration into account.
This object is exposed in templates as the urls variable, which can be used like this:
Back to the Homepage
See datasette.urls for full details on this object.",14,
184,Plugins that define new plugin hooks,"Plugins can define new plugin hooks that other plugins can use to further extend their functionality.
datasette-graphql is one example of a plugin that does this. It defines a new hook called graphql_extra_fields , described here , which other plugins can use to define additional fields that should be included in the GraphQL schema.
To define additional hooks, add a file to the plugin called datasette_your_plugin/hookspecs.py with content that looks like this:
from pluggy import HookspecMarker
hookspec = HookspecMarker(""datasette"")
@hookspec
def name_of_your_hook_goes_here(datasette):
""Description of your hook.""
You should define your own hook name and arguments here, following the documentation for Pluggy specifications . Make sure to pick a name that is unlikely to clash with hooks provided by any other plugins.
Then, to register your plugin hooks, add the following code to your datasette_your_plugin/__init__.py file:
from datasette.plugins import pm
from . import hookspecs
pm.add_hookspecs(hookspecs)
This will register your plugin hooks as part of the datasette plugin hook namespace.
Within your plugin code you can trigger the hook using this pattern:
from datasette.plugins import pm
for (
plugin_return_value
) in pm.hook.name_of_your_hook_goes_here(
datasette=datasette
):
# Do something with plugin_return_value
pass
Other plugins will then be able to register their own implementations of your hook using this syntax:
from datasette import hookimpl
@hookimpl
def name_of_your_hook_goes_here(datasette):
return ""Response from this plugin hook""
These plugin implementations can accept 0 or more of the named arguments that you defined in your hook specification.",14,
185,Deploying Datasette,"The quickest way to deploy a Datasette instance on the internet is to use the datasette publish command, described in Publishing data . This can be used to quickly deploy Datasette to a number of hosting providers including Heroku, Google Cloud Run and Vercel.
You can deploy Datasette to other hosting providers using the instructions on this page.",14,
186,Deployment fundamentals,"Datasette can be deployed as a single datasette process that listens on a port. Datasette is not designed to be run as root, so that process should listen on a higher port such as port 8000.
If you want to serve Datasette on port 80 (the HTTP default port) or port 443 (for HTTPS) you should run it behind a proxy server, such as nginx, Apache or HAProxy. The proxy server can listen on port 80/443 and forward traffic on to Datasette.",14,
187,Running Datasette using systemd,"You can run Datasette on Ubuntu or Debian systems using systemd .
First, ensure you have Python 3 and pip installed. On Ubuntu you can use sudo apt-get install python3 python3-pip .
You can install Datasette into a virtual environment, or you can install it system-wide. To install system-wide, use sudo pip3 install datasette .
Now create a folder for your Datasette databases, for example using mkdir /home/ubuntu/datasette-root .
You can copy a test database into that folder like so:
cd /home/ubuntu/datasette-root
curl -O https://latest.datasette.io/fixtures.db
Create a file at /etc/systemd/system/datasette.service with the following contents:
[Unit]
Description=Datasette
After=network.target
[Service]
Type=simple
User=ubuntu
Environment=DATASETTE_SECRET=
WorkingDirectory=/home/ubuntu/datasette-root
ExecStart=datasette serve . -h 127.0.0.1 -p 8000
Restart=on-failure
[Install]
WantedBy=multi-user.target
Add a random value for the DATASETTE_SECRET - this will be used to sign Datasette cookies such as the CSRF token cookie. You can generate a suitable value like so:
python3 -c 'import secrets; print(secrets.token_hex(32))'
This configuration will run Datasette against all database files contained in the /home/ubuntu/datasette-root directory. If that directory contains a metadata.yml (or .json ) file or a templates/ or plugins/ sub-directory those will automatically be loaded by Datasette - see Configuration directory mode for details.
You can start the Datasette process running using the following:
sudo systemctl daemon-reload
sudo systemctl start datasette.service
You will need to restart the Datasette service after making changes to its metadata.json configuration or adding a new database file to that directory. You can do that using:
sudo systemctl restart datasette.service
Once the service has started you can confirm that Datasette is running on port 8000 like so:
curl 127.0.0.1:8000/-/versions.json
# Should output JSON showing the installed version
Datasette will not be accessible from outside the server because it is listening on 127.0.0.1 . You can expose it by instead listening on 0.0.0.0 , but a better way is to set up a proxy such as nginx - see Running Datasette behind a proxy .",14,
188,Running Datasette using OpenRC,"OpenRC is the service manager on non-systemd Linux distributions like Alpine Linux and Gentoo .
Create an init script at /etc/init.d/datasette with the following contents:
#!/sbin/openrc-run
name=""datasette""
command=""datasette""
command_args=""serve -h 0.0.0.0 /path/to/db.db""
command_background=true
pidfile=""/run/${RC_SVCNAME}.pid""
You then need to configure the service to run at boot and start it:
rc-update add datasette
rc-service datasette start",14,
189,Deploying using buildpacks,"Some hosting providers such as Heroku , DigitalOcean App Platform and Scalingo support the Buildpacks standard for deploying Python web applications.
Deploying Datasette on these platforms requires two files: requirements.txt and Procfile .
The requirements.txt file lets the platform know which Python packages should be installed. It should contain datasette at a minimum, but can also list any Datasette plugins you wish to install - for example:
datasette
datasette-vega
The Procfile lets the hosting platform know how to run the command that serves web traffic. It should look like this:
web: datasette . -h 0.0.0.0 -p $PORT --cors
The $PORT environment variable is provided by the hosting platform. --cors enables CORS requests from JavaScript running on other websites to your domain - omit this if you don't want to allow CORS. You can add additional Datasette Settings options here too.
These two files should be enough to deploy Datasette on any host that supports buildpacks. Datasette will serve any SQLite files that are included in the root directory of the application.
If you want to build SQLite files or download them as part of the deployment process you can do so using a bin/post_compile file. For example, the following bin/post_compile will download an example database that will then be served by Datasette:
wget https://fivethirtyeight.datasettes.com/fivethirtyeight.db
simonw/buildpack-datasette-demo is an example GitHub repository showing a Datasette configuration that can be deployed to a buildpack-supporting host.",14,
190,Running Datasette behind a proxy,"You may wish to run Datasette behind an Apache or nginx proxy, using a path within your existing site.
You can use the base_url configuration setting to tell Datasette to serve traffic with a specific URL prefix. For example, you could run Datasette like this:
datasette my-database.db --setting base_url /my-datasette/ -p 8009
This will run Datasette with the following URLs:
http://127.0.0.1:8009/my-datasette/ - the Datasette homepage
http://127.0.0.1:8009/my-datasette/my-database - the page for the my-database.db database
http://127.0.0.1:8009/my-datasette/my-database/some_table - the page for the some_table table
You can now set your nginx or Apache server to proxy the /my-datasette/ path to this Datasette instance.",14,
191,Nginx proxy configuration,"Here is an example of an nginx configuration file that will proxy traffic to Datasette:
daemon off;
events {
worker_connections 1024;
}
http {
server {
listen 80;
location /my-datasette {
proxy_pass http://127.0.0.1:8009/my-datasette;
proxy_set_header Host $host;
}
}
}
You can also use the --uds option to Datasette to listen on a Unix domain socket instead of a port, configuring the nginx upstream proxy like this:
daemon off;
events {
worker_connections 1024;
}
http {
server {
listen 80;
location /my-datasette {
proxy_pass http://datasette/my-datasette;
proxy_set_header Host $host;
}
}
upstream datasette {
server unix:/tmp/datasette.sock;
}
}
Then run Datasette with datasette --uds /tmp/datasette.sock path/to/database.db --setting base_url /my-datasette/ .",14,
192,Apache proxy configuration,"For Apache , you can use the ProxyPass directive. First make sure the following lines are uncommented:
LoadModule proxy_module lib/httpd/modules/mod_proxy.so
LoadModule proxy_http_module lib/httpd/modules/mod_proxy_http.so
Then add these directives to proxy traffic:
ProxyPass /my-datasette/ http://127.0.0.1:8009/my-datasette/
ProxyPreserveHost On
A live demo of Datasette running behind Apache using this proxy setup can be seen at datasette-apache-proxy-demo.datasette.io/prefix/ . The code for that demo can be found in the demos/apache-proxy directory.
Using --uds you can use Unix domain sockets similar to the nginx example:
ProxyPass /my-datasette/ unix:/tmp/datasette.sock|http://localhost/my-datasette/
The ProxyPreserveHost On directive ensures that the original Host: header from the incoming request is passed through to Datasette. Datasette needs this to correctly assemble links to other pages using the .absolute_url(request, path) method.",14,
193,Introspection,"Datasette includes some pages and JSON API endpoints for introspecting the current instance. These can be used to understand some of the internals of Datasette and to see how a particular instance has been configured.
Each of these pages can be viewed in your browser. Add .json to the URL to get back the contents as JSON.",14,
194,/-/metadata,"Shows the contents of the metadata.json file that was passed to datasette serve , if any. Metadata example :
{
""license"": ""CC Attribution 4.0 License"",
""license_url"": ""http://creativecommons.org/licenses/by/4.0/"",
""source"": ""fivethirtyeight/data on GitHub"",
""source_url"": ""https://github.com/fivethirtyeight/data"",
""title"": ""Five Thirty Eight"",
""databases"": {
}
}",14,
195,/-/versions,"Shows the version of Datasette, Python and SQLite. Versions example :
{
""datasette"": {
""version"": ""0.60""
},
""python"": {
""full"": ""3.8.12 (default, Dec 21 2021, 10:45:09) \n[GCC 10.2.1 20210110]"",
""version"": ""3.8.12""
},
""sqlite"": {
""extensions"": {
""json1"": null
},
""fts_versions"": [
""FTS5"",
""FTS4"",
""FTS3""
],
""compile_options"": [
""COMPILER=gcc-6.3.0 20170516"",
""ENABLE_FTS3"",
""ENABLE_FTS4"",
""ENABLE_FTS5"",
""ENABLE_JSON1"",
""ENABLE_RTREE"",
""THREADSAFE=1""
],
""version"": ""3.37.0""
}
}",14,
196,/-/plugins,"Shows a list of currently installed plugins and their versions. Plugins example :
[
{
""name"": ""datasette_cluster_map"",
""static"": true,
""templates"": false,
""version"": ""0.10"",
""hooks"": [""extra_css_urls"", ""extra_js_urls"", ""extra_body_script""]
}
]
Add ?all=1 to include details of the default plugins baked into Datasette.",14,
197,/-/settings,"Shows the Settings for this instance of Datasette. Settings example :
{
""default_facet_size"": 30,
""default_page_size"": 100,
""facet_suggest_time_limit_ms"": 50,
""facet_time_limit_ms"": 1000,
""max_returned_rows"": 1000,
""sql_time_limit_ms"": 1000
}",14,
198,/-/config,"Shows the configuration for this instance of Datasette. This is generally the contents of the datasette.yaml or datasette.json file, which can include plugin configuration as well. Config example :
{
""settings"": {
""template_debug"": true,
""trace_debug"": true,
""force_https_urls"": true
}
}
Any keys that include the one of the following substrings in their names will be returned as redacted *** output, to help avoid accidentally leaking private configuration information: secret , key , password , token , hash , dsn .",14,
199,/-/databases,"Shows currently attached databases. Databases example :
[
{
""hash"": null,
""is_memory"": false,
""is_mutable"": true,
""name"": ""fixtures"",
""path"": ""fixtures.db"",
""size"": 225280
}
]",14,
200,/-/threads,"Shows details of threads and asyncio tasks. Threads example :
{
""num_threads"": 2,
""threads"": [
{
""daemon"": false,
""ident"": 4759197120,
""name"": ""MainThread""
},
{
""daemon"": true,
""ident"": 123145319682048,
""name"": ""Thread-1""
},
],
""num_tasks"": 3,
""tasks"": [
"" cb=[set.discard()]>"",
"" wait_for=()]> cb=[run_until_complete..()]>"",
"" wait_for=()]>>""
]
}",14,
201,/-/actor,"Shows the currently authenticated actor. Useful for debugging Datasette authentication plugins.
{
""actor"": {
""id"": 1,
""username"": ""some-user""
}
}",14,
202,/-/messages,"The debug tool at /-/messages can be used to set flash messages to try out that feature. See .add_message(request, message, type=datasette.INFO) for details of this feature.",14,
203,Getting started,,14,
204,Play with a live demo,"The best way to experience Datasette for the first time is with a demo:
global-power-plants.datasettes.com provides a searchable database of power plants around the world, using data from the World Resources Institude rendered using the datasette-cluster-map plugin.
fivethirtyeight.datasettes.com shows Datasette running against over 400 datasets imported from the FiveThirtyEight GitHub repository .",14,
205,Follow a tutorial,"Datasette has several tutorials to help you get started with the tool. Try one of the following:
Exploring a database with Datasette shows how to use the Datasette web interface to explore a new database.
Learn SQL with Datasette introduces SQL, and shows how to use that query language to ask questions of your data.
Cleaning data with sqlite-utils and Datasette guides you through using sqlite-utils to turn a CSV file into a database that you can explore using Datasette.",14,
206,Datasette in your browser with Datasette Lite,"Datasette Lite is Datasette packaged using WebAssembly so that it runs entirely in your browser, no Python web application server required.
You can pass a URL to a CSV, SQLite or raw SQL file directly to Datasette Lite to explore that data in your browser.
This example link opens Datasette Lite and loads the SQL Murder Mystery example database from Northwestern University Knight Lab .",14,
207,Try Datasette without installing anything using Glitch,"Glitch is a free online tool for building web apps directly from your web browser. You can use Glitch to try out Datasette without needing to install any software on your own computer.
Here's a demo project on Glitch which you can use as the basis for your own experiments:
glitch.com/~datasette-csvs
Glitch allows you to ""remix"" any project to create your own copy and start editing it in your browser. You can remix the datasette-csvs project by clicking this button:
Find a CSV file and drag it onto the Glitch file explorer panel - datasette-csvs will automatically convert it to a SQLite database (using sqlite-utils ) and allow you to start exploring it using Datasette.
If your CSV file has a latitude and longitude column you can visualize it on a map by uncommenting the datasette-cluster-map line in the requirements.txt file using the Glitch file editor.
Need some data? Try this Public Art Data for the city of Seattle - hit ""Export"" and select ""CSV"" to download it as a CSV file.
For more on how this works, see Running Datasette on Glitch .",14,
208,Using Datasette on your own computer,"First, follow the Installation instructions. Now you can run Datasette against a SQLite file on your computer using the following command:
datasette path/to/database.db
This will start a web server on port 8001 - visit http://localhost:8001/
to access the web interface.
Add -o to open your browser automatically once Datasette has started:
datasette path/to/database.db -o
Use Chrome on OS X? You can run datasette against your browser history
like so:
datasette ~/Library/Application\ Support/Google/Chrome/Default/History --nolock
The --nolock option ignores any file locks. This is safe as Datasette will open the file in read-only mode.
Now visiting http://localhost:8001/History/downloads will show you a web
interface to browse your downloads data:
http://localhost:8001/History/downloads.json will return that data as
JSON:
{
""database"": ""History"",
""columns"": [
""id"",
""current_path"",
""target_path"",
""start_time"",
""received_bytes"",
""total_bytes"",
...
],
""rows"": [
[
1,
""/Users/simonw/Downloads/DropboxInstaller.dmg"",
""/Users/simonw/Downloads/DropboxInstaller.dmg"",
13097290269022132,
626688,
0,
...
]
]
}
http://localhost:8001/History/downloads.json?_shape=objects will return that data as
JSON in a more convenient format:
{
...
""rows"": [
{
""start_time"": 13097290269022132,
""interrupt_reason"": 0,
""hash"": """",
""id"": 1,
""site_url"": """",
""referrer"": ""https://www.dropbox.com/downloading?src=index"",
...
}
]
}",14,
209,Internals for plugins,Many Plugin hooks are passed objects that provide access to internal Datasette functionality. The interface to these objects should not be considered stable with the exception of methods that are documented here.,14,
210,Request object,"The request object is passed to various plugin hooks. It represents an incoming HTTP request. It has the following properties:
.scope - dictionary
The ASGI scope that was used to construct this request, described in the ASGI HTTP connection scope specification.
.method - string
The HTTP method for this request, usually GET or POST .
.url - string
The full URL for this request, e.g. https://latest.datasette.io/fixtures .
.scheme - string
The request scheme - usually https or http .
.headers - dictionary (str -> str)
A dictionary of incoming HTTP request headers. Header names have been converted to lowercase.
.cookies - dictionary (str -> str)
A dictionary of incoming cookies
.host - string
The host header from the incoming request, e.g. latest.datasette.io or localhost .
.path - string
The path of the request excluding the query string, e.g. /fixtures .
.full_path - string
The path of the request including the query string if one is present, e.g. /fixtures?sql=select+sqlite_version() .
.query_string - string
The query string component of the request, without the ? - e.g. name__contains=sam&age__gt=10 .
.args - MultiParams
An object representing the parsed query string parameters, see below.
.url_vars - dictionary (str -> str)
Variables extracted from the URL path, if that path was defined using a regular expression. See register_routes(datasette) .
.actor - dictionary (str -> Any) or None
The currently authenticated actor (see actors ), or None if the request is unauthenticated.
The object also has two awaitable methods:
await request.post_vars() - dictionary
Returns a dictionary of form variables that were submitted in the request body via POST . Don't forget to read about CSRF protection !
await request.post_body() - bytes
Returns the un-parsed body of a request submitted by POST - useful for things like incoming JSON data.
And a class method that can be used to create fake request objects for use in tests:
fake(path_with_query_string, method=""GET"", scheme=""http"", url_vars=None)
Returns a Request instance for the specified path and method. For example:
from datasette import Request
from pprint import pprint
request = Request.fake(
""/fixtures/facetable/"",
url_vars={""database"": ""fixtures"", ""table"": ""facetable""},
)
pprint(request.scope)
This outputs:
{'http_version': '1.1',
'method': 'GET',
'path': '/fixtures/facetable/',
'query_string': b'',
'raw_path': b'/fixtures/facetable/',
'scheme': 'http',
'type': 'http',
'url_route': {'kwargs': {'database': 'fixtures', 'table': 'facetable'}}}",14,
211,The MultiParams class,"request.args is a MultiParams object - a dictionary-like object which provides access to query string parameters that may have multiple values.
Consider the query string ?foo=1&foo=2&bar=3 - with two values for foo and one value for bar .
request.args[key] - string
Returns the first value for that key, or raises a KeyError if the key is missing. For the above example request.args[""foo""] would return ""1"" .
request.args.get(key) - string or None
Returns the first value for that key, or None if the key is missing. Pass a second argument to specify a different default, e.g. q = request.args.get(""q"", """") .
request.args.getlist(key) - list of strings
Returns the list of strings for that key. request.args.getlist(""foo"") would return [""1"", ""2""] in the above example. request.args.getlist(""bar"") would return [""3""] . If the key is missing an empty list will be returned.
request.args.keys() - list of strings
Returns the list of available keys - for the example this would be [""foo"", ""bar""] .
key in request.args - True or False
You can use if key in request.args to check if a key is present.
for key in request.args - iterator
This lets you loop through every available key.
len(request.args) - integer
Returns the number of keys.",14,
212,Response class,"The Response class can be returned from view functions that have been registered using the register_routes(datasette) hook.
The Response() constructor takes the following arguments:
body - string
The body of the response.
status - integer (optional)
The HTTP status - defaults to 200.
headers - dictionary (optional)
A dictionary of extra HTTP headers, e.g. {""x-hello"": ""world""} .
content_type - string (optional)
The content-type for the response. Defaults to text/plain .
For example:
from datasette.utils.asgi import Response
response = Response(
""This is XML"",
content_type=""application/xml; charset=utf-8"",
)
The quickest way to create responses is using the Response.text(...) , Response.html(...) , Response.json(...) or Response.redirect(...) helper methods:
from datasette.utils.asgi import Response
html_response = Response.html(""This is HTML"")
json_response = Response.json({""this_is"": ""json""})
text_response = Response.text(
""This will become utf-8 encoded text""
)
# Redirects are served as 302, unless you pass status=301:
redirect_response = Response.redirect(
""https://latest.datasette.io/""
)
Each of these responses will use the correct corresponding content-type - text/html; charset=utf-8 , application/json; charset=utf-8 or text/plain; charset=utf-8 respectively.
Each of the helper methods take optional status= and headers= arguments, documented above.",14,
213,Returning a response with .asgi_send(send),"In most cases you will return Response objects from your own view functions. You can also use a Response instance to respond at a lower level via ASGI, for example if you are writing code that uses the asgi_wrapper(datasette) hook.
Create a Response object and then use await response.asgi_send(send) , passing the ASGI send function. For example:
async def require_authorization(scope, receive, send):
response = Response.text(
""401 Authorization Required"",
headers={
""www-authenticate"": 'Basic realm=""Datasette"", charset=""UTF-8""'
},
status=401,
)
await response.asgi_send(send)",14,
214,Setting cookies with response.set_cookie(),"To set cookies on the response, use the response.set_cookie(...) method. The method signature looks like this:
def set_cookie(
self,
key,
value="""",
max_age=None,
expires=None,
path=""/"",
domain=None,
secure=False,
httponly=False,
samesite=""lax"",
): ...
You can use this with datasette.sign() to set signed cookies. Here's how you would set the ds_actor cookie for use with Datasette authentication :
response = Response.redirect(""/"")
response.set_cookie(
""ds_actor"",
datasette.sign({""a"": {""id"": ""cleopaws""}}, ""actor""),
)
return response",14,
215,Datasette class,"This object is an instance of the Datasette class, passed to many plugin hooks as an argument called datasette .
You can create your own instance of this - for example to help write tests for a plugin - like so:
from datasette.app import Datasette
# With no arguments a single in-memory database will be attached
datasette = Datasette()
# The files= argument can load files from disk
datasette = Datasette(files=[""/path/to/my-database.db""])
# Pass metadata as a JSON dictionary like this
datasette = Datasette(
files=[""/path/to/my-database.db""],
metadata={
""databases"": {
""my-database"": {
""description"": ""This is my database""
}
}
},
)
Constructor parameters include:
files=[...] - a list of database files to open
immutables=[...] - a list of database files to open in immutable mode
metadata={...} - a dictionary of Metadata
config_dir=... - the configuration directory to use, stored in datasette.config_dir",14,
216,.databases,"Property exposing a collections.OrderedDict of databases currently connected to Datasette.
The dictionary keys are the name of the database that is used in the URL - e.g. /fixtures would have a key of ""fixtures"" . The values are Database class instances.
All databases are listed, irrespective of user permissions.",14,
217,.permissions,"Property exposing a dictionary of permissions that have been registered using the register_permissions(datasette) plugin hook.
The dictionary keys are the permission names - e.g. view-instance - and the values are Permission() objects describing the permission. Here is a description of that object .",14,
218,".plugin_config(plugin_name, database=None, table=None)","plugin_name - string
The name of the plugin to look up configuration for. Usually this is something similar to datasette-cluster-map .
database - None or string
The database the user is interacting with.
table - None or string
The table the user is interacting with.
This method lets you read plugin configuration values that were set in datasette.yaml . See Writing plugins that accept configuration for full details of how this method should be used.
The return value will be the value from the configuration file - usually a dictionary.
If the plugin is not configured the return value will be None .",14,
219,"await .render_template(template, context=None, request=None)","template - string, list of strings or jinja2.Template
The template file to be rendered, e.g. my_plugin.html . Datasette will search for this file first in the --template-dir= location, if it was specified - then in the plugin's bundled templates and finally in Datasette's set of default templates.
If this is a list of template file names then the first one that exists will be loaded and rendered.
If this is a Jinja Template object it will be used directly.
context - None or a Python dictionary
The context variables to pass to the template.
request - request object or None
If you pass a Datasette request object here it will be made available to the template.
Renders a Jinja template using Datasette's preconfigured instance of Jinja and returns the resulting string. The template will have access to Datasette's default template functions and any functions that have been made available by other plugins.",14,
220,await .actors_from_ids(actor_ids),"actor_ids - list of strings or integers
A list of actor IDs to look up.
Returns a dictionary, where the keys are the IDs passed to it and the values are the corresponding actor dictionaries.
This method is mainly designed to be used with plugins. See the actors_from_ids(datasette, actor_ids) documentation for details.
If no plugins that implement that hook are installed, the default return value looks like this:
{
""1"": {""id"": ""1""},
""2"": {""id"": ""2""}
}",14,
221,"await .permission_allowed(actor, action, resource=None, default=...)","actor - dictionary
The authenticated actor. This is usually request.actor .
action - string
The name of the action that is being permission checked.
resource - string or tuple, optional
The resource, e.g. the name of the database, or a tuple of two strings containing the name of the database and the name of the table. Only some permissions apply to a resource.
default - optional: True, False or None
What value should be returned by default if nothing provides an opinion on this permission check.
Set to True for default allow or False for default deny.
If not specified the default from the Permission() tuple that was registered using register_permissions(datasette) will be used.
Check if the given actor has permission to perform the given action on the given resource.
Some permission checks are carried out against rules defined in datasette.yaml , while other custom permissions may be decided by plugins that implement the permission_allowed(datasette, actor, action, resource) plugin hook.
If neither metadata.json nor any of the plugins provide an answer to the permission query the default argument will be returned.
See Built-in permissions for a full list of permission actions included in Datasette core.",14,
222,"await .ensure_permissions(actor, permissions)","actor - dictionary
The authenticated actor. This is usually request.actor .
permissions - list
A list of permissions to check. Each permission in that list can be a string action name or a 2-tuple of (action, resource) .
This method allows multiple permissions to be checked at once. It raises a datasette.Forbidden exception if any of the checks are denied before one of them is explicitly granted.
This is useful when you need to check multiple permissions at once. For example, an actor should be able to view a table if either one of the following checks returns True or not a single one of them returns False :
await self.ds.ensure_permissions(
request.actor,
[
(""view-table"", (database, table)),
(""view-database"", database),
""view-instance"",
],
)",14,
223,"await .check_visibility(actor, action=None, resource=None, permissions=None)","actor - dictionary
The authenticated actor. This is usually request.actor .
action - string, optional
The name of the action that is being permission checked.
resource - string or tuple, optional
The resource, e.g. the name of the database, or a tuple of two strings containing the name of the database and the name of the table. Only some permissions apply to a resource.
permissions - list of action strings or (action, resource) tuples, optional
Provide this instead of action and resource to check multiple permissions at once.
This convenience method can be used to answer the question ""should this item be considered private, in that it is visible to me but it is not visible to anonymous users?""
It returns a tuple of two booleans, (visible, private) . visible indicates if the actor can see this resource. private will be True if an anonymous user would not be able to view the resource.
This example checks if the user can access a specific table, and sets private so that a padlock icon can later be displayed:
visible, private = await self.ds.check_visibility(
request.actor,
action=""view-table"",
resource=(database, table),
)
The following example runs three checks in a row, similar to await .ensure_permissions(actor, permissions) . If any of the checks are denied before one of them is explicitly granted then visible will be False . private will be True if an anonymous user would not be able to view the resource.
visible, private = await self.ds.check_visibility(
request.actor,
permissions=[
(""view-table"", (database, table)),
(""view-database"", database),
""view-instance"",
],
)",14,
224,".create_token(actor_id, expires_after=None, restrict_all=None, restrict_database=None, restrict_resource=None)","actor_id - string
The ID of the actor to create a token for.
expires_after - int, optional
The number of seconds after which the token should expire.
restrict_all - iterable, optional
A list of actions that this token should be restricted to across all databases and resources.
restrict_database - dict, optional
For restricting actions within specific databases, e.g. {""mydb"": [""view-table"", ""view-query""]} .
restrict_resource - dict, optional
For restricting actions to specific resources (tables, SQL views and Canned queries ) within a database. For example: {""mydb"": {""mytable"": [""insert-row"", ""update-row""]}} .
This method returns a signed API token of the format dstok_... which can be used to authenticate requests to the Datasette API.
All tokens must have an actor_id string indicating the ID of the actor which the token will act on behalf of.
Tokens default to lasting forever, but can be set to expire after a given number of seconds using the expires_after argument. The following code creates a token for user1 that will expire after an hour:
token = datasette.create_token(
actor_id=""user1"",
expires_after=3600,
)
The three restrict_* arguments can be used to create a token that has additional restrictions beyond what the associated actor is allowed to do.
The following example creates a token that can access view-instance and view-table across everything, can additionally use view-query for anything in the docs database and is allowed to execute insert-row and update-row in the attachments table in that database:
token = datasette.create_token(
actor_id=""user1"",
restrict_all=(""view-instance"", ""view-table""),
restrict_database={""docs"": (""view-query"",)},
restrict_resource={
""docs"": {
""attachments"": (""insert-row"", ""update-row"")
}
},
)",14,
225,.get_permission(name_or_abbr),"name_or_abbr - string
The name or abbreviation of the permission to look up, e.g. view-table or vt .
Returns a Permission object representing the permission, or raises a KeyError if one is not found.",14,
226,.get_database(name),"name - string, optional
The name of the database - optional.
Returns the specified database object. Raises a KeyError if the database does not exist. Call this method without an argument to return the first connected database.",14,
227,.get_internal_database(),Returns a database object for reading and writing to the private internal database .,14,
228,".add_database(db, name=None, route=None)","db - datasette.database.Database instance
The database to be attached.
name - string, optional
The name to be used for this database . If not specified Datasette will pick one based on the filename or memory name.
route - string, optional
This will be used in the URL path. If not specified, it will default to the same thing as the name .
The datasette.add_database(db) method lets you add a new database to the current Datasette instance.
The db parameter should be an instance of the datasette.database.Database class. For example:
from datasette.database import Database
datasette.add_database(
Database(
datasette,
path=""path/to/my-new-database.db"",
)
)
This will add a mutable database and serve it at /my-new-database .
Use is_mutable=False to add an immutable database.
.add_database() returns the Database instance, with its name set as the database.name attribute. Any time you are working with a newly added database you should use the return value of .add_database() , for example:
db = datasette.add_database(
Database(datasette, memory_name=""statistics"")
)
await db.execute_write(
""CREATE TABLE foo(id integer primary key)""
)",14,
229,.add_memory_database(name),"Adds a shared in-memory database with the specified name:
datasette.add_memory_database(""statistics"")
This is a shortcut for the following:
from datasette.database import Database
datasette.add_database(
Database(datasette, memory_name=""statistics"")
)
Using either of these pattern will result in the in-memory database being served at /statistics .",14,
230,.remove_database(name),"name - string
The name of the database to be removed.
This removes a database that has been previously added. name= is the unique name of that database.",14,
231,await .track_event(event),"event - Event
An instance of a subclass of datasette.events.Event .
Plugins can call this to track events, using classes they have previously registered. See Event tracking for details.
The event will then be passed to all plugins that have registered to receive events using the track_event(datasette, event) hook.
Example usage, assuming the plugin has previously registered the BanUserEvent class:
await datasette.track_event(
BanUserEvent(user={""id"": 1, ""username"": ""cleverbot""})
)",14,
232,".sign(value, namespace=""default"")","value - any serializable type
The value to be signed.
namespace - string, optional
An alternative namespace, see the itsdangerous salt documentation .
Utility method for signing values, such that you can safely pass data to and from an untrusted environment. This is a wrapper around the itsdangerous library.
This method returns a signed string, which can be decoded and verified using .unsign(value, namespace=""default"") .",14,
233,".unsign(value, namespace=""default"")","signed - any serializable type
The signed string that was created using .sign(value, namespace=""default"") .
namespace - string, optional
The alternative namespace, if one was used.
Returns the original, decoded object that was passed to .sign(value, namespace=""default"") . If the signature is not valid this raises a itsdangerous.BadSignature exception.",14,
234,".add_message(request, message, type=datasette.INFO)","request - Request
The current Request object
message - string
The message string
type - constant, optional
The message type - datasette.INFO , datasette.WARNING or datasette.ERROR
Datasette's flash messaging mechanism allows you to add a message that will be displayed to the user on the next page that they visit. Messages are persisted in a ds_messages cookie. This method adds a message to that cookie.
You can try out these messages (including the different visual styling of the three message types) using the /-/messages debugging tool.",14,
235,".absolute_url(request, path)","request - Request
The current Request object
path - string
A path, for example /dbname/table.json
Returns the absolute URL for the given path, including the protocol and host. For example:
absolute_url = datasette.absolute_url(
request, ""/dbname/table.json""
)
# Would return ""http://localhost:8001/dbname/table.json""
The current request object is used to determine the hostname and protocol that should be used for the returned URL. The force_https_urls configuration setting is taken into account.",14,
236,.setting(key),"key - string
The name of the setting, e.g. base_url .
Returns the configured value for the specified setting . This can be a string, boolean or integer depending on the requested setting.
For example:
downloads_are_allowed = datasette.setting(""allow_download"")",14,
237,.resolve_database(request),"request - Request object
A request object
If you are implementing your own custom views, you may need to resolve the database that the user is requesting based on a URL path. If the regular expression for your route declares a database named group, you can use this method to resolve the database object.
This returns a Database instance.
If the database cannot be found, it raises a datasette.utils.asgi.DatabaseNotFound exception - which is a subclass of datasette.utils.asgi.NotFound with a .database_name attribute set to the name of the database that was requested.",14,
238,.resolve_table(request),"request - Request object
A request object
This assumes that the regular expression for your route declares both a database and a table named group.
It returns a ResolvedTable named tuple instance with the following fields:
db - Database
The database object
table - string
The name of the table (or view)
is_view - boolean
True if this is a view, False if it is a table
If the database or table cannot be found it raises a datasette.utils.asgi.DatabaseNotFound exception.
If the table does not exist it raises a datasette.utils.asgi.TableNotFound exception - a subclass of datasette.utils.asgi.NotFound with .database_name and .table attributes.",14,
239,.resolve_row(request),"request - Request object
A request object
This method assumes your route declares named groups for database , table and pks .
It returns a ResolvedRow named tuple instance with the following fields:
db - Database
The database object
table - string
The name of the table
sql - string
SQL snippet that can be used in a WHERE clause to select the row
params - dict
Parameters that should be passed to the SQL query
pks - list
List of primary key column names
pk_values - list
List of primary key values decoded from the URL
row - sqlite3.Row
The row itself
If the database or table cannot be found it raises a datasette.utils.asgi.DatabaseNotFound exception.
If the table does not exist it raises a datasette.utils.asgi.TableNotFound exception.
If the row cannot be found it raises a datasette.utils.asgi.RowNotFound exception. This has .database_name , .table and .pk_values attributes, extracted from the request path.",14,
240,datasette.client,"Plugins can make internal simulated HTTP requests to the Datasette instance within which they are running. This ensures that all of Datasette's external JSON APIs are also available to plugins, while avoiding the overhead of making an external HTTP call to access those APIs.
The datasette.client object is a wrapper around the HTTPX Python library , providing an async-friendly API that is similar to the widely used Requests library .
It offers the following methods:
await datasette.client.get(path, **kwargs) - returns HTTPX Response
Execute an internal GET request against that path.
await datasette.client.post(path, **kwargs) - returns HTTPX Response
Execute an internal POST request. Use data={""name"": ""value""} to pass form parameters.
await datasette.client.options(path, **kwargs) - returns HTTPX Response
Execute an internal OPTIONS request.
await datasette.client.head(path, **kwargs) - returns HTTPX Response
Execute an internal HEAD request.
await datasette.client.put(path, **kwargs) - returns HTTPX Response
Execute an internal PUT request.
await datasette.client.patch(path, **kwargs) - returns HTTPX Response
Execute an internal PATCH request.
await datasette.client.delete(path, **kwargs) - returns HTTPX Response
Execute an internal DELETE request.
await datasette.client.request(method, path, **kwargs) - returns HTTPX Response
Execute an internal request with the given HTTP method against that path.
These methods can be used with datasette.urls - for example:
table_json = (
await datasette.client.get(
datasette.urls.table(
""fixtures"", ""facetable"", format=""json""
)
)
).json()
datasette.client methods automatically take the current base_url setting into account, whether or not you use the datasette.urls family of methods to construct the path.
For documentation on available **kwargs options and the shape of the HTTPX Response object refer to the HTTPX Async documentation .",14,
241,datasette.urls,"The datasette.urls object contains methods for building URLs to pages within Datasette. Plugins should use this to link to pages, since these methods take into account any base_url configuration setting that might be in effect.
datasette.urls.instance(format=None)
Returns the URL to the Datasette instance root page. This is usually ""/"" .
datasette.urls.path(path, format=None)
Takes a path and returns the full path, taking base_url into account.
For example, datasette.urls.path(""-/logout"") will return the path to the logout page, which will be ""/-/logout"" by default or /prefix-path/-/logout if base_url is set to /prefix-path/
datasette.urls.logout()
Returns the URL to the logout page, usually ""/-/logout""
datasette.urls.static(path)
Returns the URL of one of Datasette's default static assets, for example ""/-/static/app.css""
datasette.urls.static_plugins(plugin_name, path)
Returns the URL of one of the static assets belonging to a plugin.
datasette.urls.static_plugins(""datasette_cluster_map"", ""datasette-cluster-map.js"") would return ""/-/static-plugins/datasette_cluster_map/datasette-cluster-map.js""
datasette.urls.static(path)
Returns the URL of one of Datasette's default static assets, for example ""/-/static/app.css""
datasette.urls.database(database_name, format=None)
Returns the URL to a database page, for example ""/fixtures""
datasette.urls.table(database_name, table_name, format=None)
Returns the URL to a table page, for example ""/fixtures/facetable""
datasette.urls.query(database_name, query_name, format=None)
Returns the URL to a query page, for example ""/fixtures/pragma_cache_size""
These functions can be accessed via the {{ urls }} object in Datasette templates, for example:
HomepageFixtures databasefacetable tablepragma_cache_size query
Use the format=""json"" (or ""csv"" or other formats supported by plugins) arguments to get back URLs to the JSON representation. This is the path with .json added on the end.
These methods each return a datasette.utils.PrefixedUrlString object, which is a subclass of the Python str type. This allows the logic that considers the base_url setting to detect if that prefix has already been applied to the path.",14,
242,Database class,"Instances of the Database class can be used to execute queries against attached SQLite databases, and to run introspection against their schemas.",14,
243,"Database(ds, path=None, is_mutable=True, is_memory=False, memory_name=None)","The Database() constructor can be used by plugins, in conjunction with .add_database(db, name=None, route=None) , to create and register new databases.
The arguments are as follows:
ds - Datasette class (required)
The Datasette instance you are attaching this database to.
path - string
Path to a SQLite database file on disk.
is_mutable - boolean
Set this to False to cause Datasette to open the file in immutable mode.
is_memory - boolean
Use this to create non-shared memory connections.
memory_name - string or None
Use this to create a named in-memory database. Unlike regular memory databases these can be accessed by multiple threads and will persist an changes made to them for the lifetime of the Datasette server process.
The first argument is the datasette instance you are attaching to, the second is a path= , then is_mutable and is_memory are both optional arguments.",14,
244,db.hash,"If the database was opened in immutable mode, this property returns the 64 character SHA-256 hash of the database contents as a string. Otherwise it returns None .",14,
245,"await db.execute(sql, ...)","Executes a SQL query against the database and returns the resulting rows (see Results ).
sql - string (required)
The SQL query to execute. This can include ? or :named parameters.
params - list or dict
A list or dictionary of values to use for the parameters. List for ? , dictionary for :named .
truncate - boolean
Should the rows returned by the query be truncated at the maximum page size? Defaults to True , set this to False to disable truncation.
custom_time_limit - integer ms
A custom time limit for this query. This can be set to a lower value than the Datasette configured default. If a query takes longer than this it will be terminated early and raise a dataette.database.QueryInterrupted exception.
page_size - integer
Set a custom page size for truncation, over-riding the configured Datasette default.
log_sql_errors - boolean
Should any SQL errors be logged to the console in addition to being raised as an error? Defaults to True .",14,
246,Results,"The db.execute() method returns a single Results object. This can be used to access the rows returned by the query.
Iterating over a Results object will yield SQLite Row objects . Each of these can be treated as a tuple or can be accessed using row[""column""] syntax:
info = []
results = await db.execute(""select name from sqlite_master"")
for row in results:
info.append(row[""name""])
The Results object also has the following properties and methods:
.truncated - boolean
Indicates if this query was truncated - if it returned more results than the specified page_size . If this is true then the results object will only provide access to the first page_size rows in the query result. You can disable truncation by passing truncate=False to the db.query() method.
.columns - list of strings
A list of column names returned by the query.
.rows - list of sqlite3.Row
This property provides direct access to the list of rows returned by the database. You can access specific rows by index using results.rows[0] .
.first() - row or None
Returns the first row in the results, or None if no rows were returned.
.single_value()
Returns the value of the first column of the first row of results - but only if the query returned a single row with a single column. Raises a datasette.database.MultipleValues exception otherwise.
.__len__()
Calling len(results) returns the (truncated) number of returned results.",14,
247,await db.execute_fn(fn),"Executes a given callback function against a read-only database connection running in a thread. The function will be passed a SQLite connection, and the return value from the function will be returned by the await .
Example usage:
def get_version(conn):
return conn.execute(
""select sqlite_version()""
).fetchall()[0][0]
version = await db.execute_fn(get_version)",14,
248,"await db.execute_write(sql, params=None, block=True)","SQLite only allows one database connection to write at a time. Datasette handles this for you by maintaining a queue of writes to be executed against a given database. Plugins can submit write operations to this queue and they will be executed in the order in which they are received.
This method can be used to queue up a non-SELECT SQL query to be executed against a single write connection to the database.
You can pass additional SQL parameters as a tuple or dictionary.
The method will block until the operation is completed, and the return value will be the return from calling conn.execute(...) using the underlying sqlite3 Python library.
If you pass block=False this behavior changes to ""fire and forget"" - queries will be added to the write queue and executed in a separate thread while your code can continue to do other things. The method will return a UUID representing the queued task.
Each call to execute_write() will be executed inside a transaction.",14,
249,"await db.execute_write_script(sql, block=True)","Like execute_write() but can be used to send multiple SQL statements in a single string separated by semicolons, using the sqlite3 conn.executescript() method.
Each call to execute_write_script() will be executed inside a transaction.",14,
250,"await db.execute_write_many(sql, params_seq, block=True)","Like execute_write() but uses the sqlite3 conn.executemany() method. This will efficiently execute the same SQL statement against each of the parameters in the params_seq iterator, for example:
await db.execute_write_many(
""insert into characters (id, name) values (?, ?)"",
[(1, ""Melanie""), (2, ""Selma""), (2, ""Viktor"")],
)
Each call to execute_write_many() will be executed inside a transaction.",14,
251,"await db.execute_write_fn(fn, block=True, transaction=True)","This method works like .execute_write() , but instead of a SQL statement you give it a callable Python function. Your function will be queued up and then called when the write connection is available, passing that connection as the argument to the function.
The function can then perform multiple actions, safe in the knowledge that it has exclusive access to the single writable connection for as long as it is executing.
fn needs to be a regular function, not an async def function.
For example:
def delete_and_return_count(conn):
conn.execute(""delete from some_table where id > 5"")
return conn.execute(
""select count(*) from some_table""
).fetchone()[0]
try:
num_rows_left = await database.execute_write_fn(
delete_and_return_count
)
except Exception as e:
print(""An error occurred:"", e)
The value returned from await database.execute_write_fn(...) will be the return value from your function.
If your function raises an exception that exception will be propagated up to the await line.
By default your function will be executed inside a transaction. You can pass transaction=False to disable this behavior, though if you do that you should be careful to manually apply transactions - ideally using the with conn: pattern, or you may see OperationalError: database table is locked errors.
If you specify block=False the method becomes fire-and-forget, queueing your function to be executed and then allowing your code after the call to .execute_write_fn() to continue running while the underlying thread waits for an opportunity to run your function. A UUID representing the queued task will be returned. Any exceptions in your code will be silently swallowed.",14,
252,await db.execute_isolated_fn(fn),"This method works is similar to execute_write_fn() but executes the provided function in an entirely isolated SQLite connection, which is opened, used and then closed again in a single call to this method.
The prepare_connection() plugin hook is not executed against this connection.
This allows plugins to execute database operations that might conflict with how database connections are usually configured. For example, running a VACUUM operation while bypassing any restrictions placed by the datasette-sqlite-authorizer plugin.
Plugins can also use this method to load potentially dangerous SQLite extensions, use them to perform an operation and then have them safely unloaded at the end of the call, without risk of exposing them to other connections.
Functions run using execute_isolated_fn() share the same queue as execute_write_fn() , which guarantees that no writes can be executed at the same time as the isolated function is executing.
The return value of the function will be returned by this method. Any exceptions raised by the function will be raised out of the await line as well.",14,
253,db.close(),"Closes all of the open connections to file-backed databases. This is mainly intended to be used by large test suites, to avoid hitting limits on the number of open files.",14,
254,Database introspection,"The Database class also provides properties and methods for introspecting the database.
db.name - string
The name of the database - usually the filename without the .db prefix.
db.size - integer
The size of the database file in bytes. 0 for :memory: databases.
db.mtime_ns - integer or None
The last modification time of the database file in nanoseconds since the epoch. None for :memory: databases.
db.is_mutable - boolean
Is this database mutable, and allowed to accept writes?
db.is_memory - boolean
Is this database an in-memory database?
await db.attached_databases() - list of named tuples
Returns a list of additional databases that have been connected to this database using the SQLite ATTACH command. Each named tuple has fields seq , name and file .
await db.table_exists(table) - boolean
Check if a table called table exists.
await db.view_exists(view) - boolean
Check if a view called view exists.
await db.table_names() - list of strings
List of names of tables in the database.
await db.view_names() - list of strings
List of names of views in the database.
await db.table_columns(table) - list of strings
Names of columns in a specific table.
await db.table_column_details(table) - list of named tuples
Full details of the columns in a specific table. Each column is represented by a Column named tuple with fields cid (integer representing the column position), name (string), type (string, e.g. REAL or VARCHAR(30) ), notnull (integer 1 or 0), default_value (string or None), is_pk (integer 1 or 0).
await db.primary_keys(table) - list of strings
Names of the columns that are part of the primary key for this table.
await db.fts_table(table) - string or None
The name of the FTS table associated with this table, if one exists.
await db.label_column_for_table(table) - string or None
The label column that is associated with this table - either automatically detected or using the ""label_column"" key from Metadata , see Specifying the label column for a table .
await db.foreign_keys_for_table(table) - list of dictionaries
Details of columns in this table which are foreign keys to other tables. A list of dictionaries where each dictionary is shaped like this: {""column"": string, ""other_table"": string, ""other_column"": string} .
await db.hidden_table_names() - list of strings
List of tables which Datasette ""hides"" by default - usually these are tables associated with SQLite's full-text search feature, the SpatiaLite extension or tables hidden using the Hiding tables feature.
await db.get_table_definition(table) - string
Returns the SQL definition for the table - the CREATE TABLE statement and any associated CREATE INDEX statements.
await db.get_view_definition(view) - string
Returns the SQL definition of the named view.
await db.get_all_foreign_keys() - dictionary
Dictionary representing both incoming and outgoing foreign keys for this table. It has two keys, ""incoming"" and ""outgoing"" , each of which is a list of dictionaries with keys ""column"" , ""other_table"" and ""other_column"" . For example:
{
""incoming"": [],
""outgoing"": [
{
""other_table"": ""attraction_characteristic"",
""column"": ""characteristic_id"",
""other_column"": ""pk"",
},
{
""other_table"": ""roadside_attractions"",
""column"": ""attraction_id"",
""other_column"": ""pk"",
}
]
}",14,
255,CSRF protection,"Datasette uses asgi-csrf to guard against CSRF attacks on form POST submissions. Users receive a ds_csrftoken cookie which is compared against the csrftoken form field (or x-csrftoken HTTP header) for every incoming request.
If your plugin implements a