id,page,ref,title,content,breadcrumbs,references events:id1,events,id1,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.",[],[] facets:facets-in-query-strings,facets,facets-in-query-strings,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).","[""Facets""]",[] facets:facets-metadata,facets,facets-metadata,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]]]","[""Facets""]",[] facets:suggested-facets,facets,suggested-facets,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.","[""Facets""]",[] full_text_search:full-text-search-fts-versions,full_text_search,full-text-search-fts-versions,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.","[""Full-text search""]",[] plugin_hooks:plugin-page-extras,plugin_hooks,plugin-page-extras,Page extras,These plugin hooks can be used to affect the way HTML pages for different Datasette interfaces are rendered.,"[""Plugin hooks""]",[] plugin_hooks:plugin-register-permissions,plugin_hooks,plugin-register-permissions,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.","[""Plugin hooks""]",[] plugin_hooks:plugin-hook-register-magic-parameters,plugin_hooks,plugin-hook-register-magic-parameters,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), ]","[""Plugin hooks""]",[] plugin_hooks:plugin-hook-forbidden,plugin_hooks,plugin-hook-forbidden,"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","[""Plugin hooks""]",[] plugin_hooks:plugin-actions,plugin_hooks,plugin-actions,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.","[""Plugin hooks""]",[] plugin_hooks:plugin-hook-view-actions,plugin_hooks,plugin-hook-view-actions,"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.","[""Plugin hooks"", ""Action hooks""]",[] plugin_hooks:plugin-hook-homepage-actions,plugin_hooks,plugin-hook-homepage-actions,"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"", } ]","[""Plugin hooks"", ""Action hooks""]",[] plugin_hooks:plugin-hook-top-homepage,plugin_hooks,plugin-hook-top-homepage,"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.","[""Plugin hooks"", ""Template slots""]",[] plugin_hooks:plugin-hook-top-database,plugin_hooks,plugin-hook-top-database,"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.","[""Plugin hooks"", ""Template slots""]",[] plugin_hooks:plugin-hook-top-table,plugin_hooks,plugin-hook-top-table,"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.","[""Plugin hooks"", ""Template slots""]",[] plugin_hooks:plugin-hook-top-row,plugin_hooks,plugin-hook-top-row,"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.","[""Plugin hooks"", ""Template slots""]",[] plugin_hooks:plugin-hook-top-query,plugin_hooks,plugin-hook-top-query,"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.","[""Plugin hooks"", ""Template slots""]",[] plugin_hooks:plugin-hook-top-canned-query,plugin_hooks,plugin-hook-top-canned-query,"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.","[""Plugin hooks"", ""Template slots""]",[] plugin_hooks:plugin-event-tracking,plugin_hooks,plugin-event-tracking,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 .","[""Plugin hooks""]",[] plugin_hooks:plugin-hook-register-events,plugin_hooks,plugin-hook-register-events,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""}) )","[""Plugin hooks"", ""Event tracking""]",[] installation:id1,installation,id1,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",[],[] installation:installation-basic,installation,installation-basic,Basic installation,,"[""Installation""]",[] installation:installation-advanced,installation,installation-advanced,Advanced installation options,,"[""Installation""]",[] installation:installing-plugins-using-pipx,installation,installing-plugins-using-pipx,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"" } ]","[""Installation"", ""Advanced installation options"", ""Using pipx""]",[] installation:upgrading-packages-using-pipx,installation,upgrading-packages-using-pipx,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"" } ]","[""Installation"", ""Advanced installation options"", ""Using pipx""]",[] installation:installation-extensions,installation,installation-extensions,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","[""Installation""]",[] testing_plugins:testing-plugins-datasette-test-instance,testing_plugins,testing-plugins-datasette-test-instance,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.","[""Testing plugins""]",[] testing_plugins:testing-plugins-pdb,testing_plugins,testing-plugins-pdb,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.","[""Testing plugins""]",[] testing_plugins:testing-plugins-register-in-test,testing_plugins,testing-plugins-register-in-test,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","[""Testing plugins""]",[] spatialite:spatialite-installation,spatialite,spatialite-installation,Installation,,"[""SpatiaLite""]",[] spatialite:installing-spatialite-on-linux,spatialite,installing-spatialite-on-linux,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.","[""SpatiaLite"", ""Installation""]",[] spatialite:spatial-indexing-latitude-longitude-columns,spatialite,spatial-indexing-latitude-longitude-columns,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()","[""SpatiaLite""]",[] spatialite:querying-polygons-using-within,spatialite,querying-polygons-using-within,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 );","[""SpatiaLite""]",[] contributing:id1,contributing,id1,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 .",[],[] contributing:general-guidelines,contributing,general-guidelines,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.","[""Contributing""]",[] contributing:contributing-using-fixtures,contributing,contributing-using-fixtures,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/","[""Contributing""]",[] contributing:contributing-debugging,contributing,contributing-debugging,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","[""Contributing""]",[] contributing:contributing-formatting-black,contributing,contributing-formatting-black,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.","[""Contributing"", ""Code formatting""]",[] cli-reference:id1,cli-reference,id1,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]]]",[],[] cli-reference:cli-help-help,cli-reference,cli-help-help,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.","[""CLI reference""]",[] cli-reference:cli-help-serve-help,cli-reference,cli-help-serve-help,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]]]","[""CLI reference""]",[] cli-reference:cli-datasette-get,cli-reference,cli-datasette-get,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.","[""CLI reference"", ""datasette serve""]",[] cli-reference:cli-help-serve-help-settings,cli-reference,cli-help-serve-help-settings,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]]]","[""CLI reference"", ""datasette serve""]",[] cli-reference:cli-help-plugins-help,cli-reference,cli-help-plugins-help,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"" ] } ]","[""CLI reference""]",[] cli-reference:cli-help-uninstall-help,cli-reference,cli-help-uninstall-help,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]]]","[""CLI reference""]",[] cli-reference:cli-help-publish-help,cli-reference,cli-help-publish-help,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]]]","[""CLI reference""]",[] cli-reference:cli-help-publish-cloudrun-help,cli-reference,cli-help-publish-cloudrun-help,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]]]","[""CLI reference""]",[] cli-reference:cli-help-publish-heroku-help,cli-reference,cli-help-publish-heroku-help,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]]]","[""CLI reference""]",[] cli-reference:cli-help-package-help,cli-reference,cli-help-package-help,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]]]","[""CLI reference""]",[] cli-reference:cli-help-inspect-help,cli-reference,cli-help-inspect-help,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]]]","[""CLI reference""]",[] cli-reference:cli-help-create-token-help,cli-reference,cli-help-create-token-help,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]]]","[""CLI reference""]",[] metadata:id1,metadata,id1,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.",[],[] metadata:per-database-and-per-table-metadata,metadata,per-database-and-per-table-metadata,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.","[""Metadata""]",[] metadata:metadata-source-license-about,metadata,metadata-source-license-about,"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.","[""Metadata""]",[] metadata:metadata-default-sort,metadata,metadata-default-sort,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]]]","[""Metadata""]",[] metadata:metadata-page-size,metadata,metadata-page-size,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.","[""Metadata""]",[] metadata:metadata-sortable-columns,metadata,metadata-sortable-columns,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]]]","[""Metadata""]",[] metadata:label-columns,metadata,label-columns,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]]]","[""Metadata""]",[] metadata:metadata-hiding-tables,metadata,metadata-hiding-tables,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]]]","[""Metadata""]",[] metadata:id2,metadata,id2,Metadata reference,A full reference of every supported option in a metadata.json or metadata.yaml file.,"[""Metadata""]",[] metadata:top-level-metadata,metadata,top-level-metadata,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","[""Metadata"", ""Metadata reference""]",[] metadata:database-level-metadata,metadata,database-level-metadata,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","[""Metadata"", ""Metadata reference""]",[] metadata:table-level-metadata,metadata,table-level-metadata,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","[""Metadata"", ""Metadata reference""]",[] sql_queries:sql,sql_queries,sql,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.",[],[] sql_queries:id1,sql_queries,id1,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).","[""Running SQL queries""]",[] sql_queries:canned-queries-options,sql_queries,canned-queries-options,Additional canned query options,Additional options can be specified for canned queries in the YAML or JSON configuration.,"[""Running SQL queries"", ""Canned queries""]",[] sql_queries:hide-sql,sql_queries,hide-sql,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.","[""Running SQL queries"", ""Canned queries"", ""Additional canned query options""]",[] sql_queries:canned-queries-writable,sql_queries,canned-queries-writable,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.","[""Running SQL queries"", ""Canned queries""]",[] sql_queries:canned-queries-magic-parameters,sql_queries,canned-queries-magic-parameters,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.","[""Running SQL queries"", ""Canned queries""]",[] sql_queries:canned-queries-json-api,sql_queries,canned-queries-json-api,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.","[""Running SQL queries"", ""Canned queries""]",[] sql_queries:id2,sql_queries,id2,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.","[""Running SQL queries""]",[] javascript_plugins:id1,javascript_plugins,id1,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.",[],[] javascript_plugins:javascript-datasette-init,javascript_plugins,javascript-datasette-init,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); });","[""JavaScript plugins""]",[] javascript_plugins:javascript-datasette-manager,javascript_plugins,javascript-datasette-manager,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","[""JavaScript plugins""]",[] javascript_plugins:id2,javascript_plugins,id2,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:","[""JavaScript plugins""]",[] javascript_plugins:javascript-plugins-makeabovetablepanelconfigs,javascript_plugins,javascript-plugins-makeabovetablepanelconfigs,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.","[""JavaScript plugins"", ""JavaScript plugin objects""]",[] javascript_plugins:javascript-plugins-makecolumnactions,javascript_plugins,javascript-plugins-makecolumnactions,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)) } ]; } }); });","[""JavaScript plugins"", ""JavaScript plugin objects""]",[] javascript_plugins:javascript-datasette-manager-selectors,javascript_plugins,javascript-datasette-manager-selectors,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]"", };","[""JavaScript plugins""]",[] custom_templates:customization,custom_templates,customization,Custom pages and templates,Datasette provides a number of ways of customizing the way data is displayed.,[],[] custom_templates:css-classes-on-the-body,custom_templates,css-classes-on-the-body,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:
id name
1 SMITH
","[""Custom pages and templates""]",[] custom_templates:customization-static-files,custom_templates,customization-static-files,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]]]","[""Custom pages and templates""]",[] custom_templates:publishing-static-assets,custom_templates,publishing-static-assets,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/ .","[""Custom pages and templates""]",[] custom_templates:id1,custom_templates,id1,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 .","[""Custom pages and templates"", ""Publishing static assets""]",[] custom_templates:custom-pages-headers,custom_templates,custom-pages-headers,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","[""Custom pages and templates""]",[] custom_templates:custom-pages-404,custom_templates,custom-pages-404,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.","[""Custom pages and templates""]",[] custom_templates:custom-pages-redirects,custom_templates,custom-pages-redirects,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) }}","[""Custom pages and templates""]",[] writing_plugins:writing-plugins-tracing,writing_plugins,writing-plugins-tracing,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'}]","[""Writing plugins""]",[] writing_plugins:writing-plugins-configuration,writing_plugins,writing-plugins-configuration,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.","[""Writing plugins""]",[] writing_plugins:writing-plugins-building-urls,writing_plugins,writing-plugins-building-urls,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.","[""Writing plugins""]",[] deploying:deploying,deploying,deploying,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.",[],[] deploying:deploying-fundamentals,deploying,deploying-fundamentals,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.","[""Deploying Datasette""]",[] deploying:deploying-systemd,deploying,deploying-systemd,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 .","[""Deploying Datasette""]",[] deploying:deploying-proxy,deploying,deploying-proxy,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.","[""Deploying Datasette""]",[] introspection:id1,introspection,id1,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.",[],[] introspection:jsondataview-actor,introspection,jsondataview-actor,/-/actor,"Shows the currently authenticated actor. Useful for debugging Datasette authentication plugins. { ""actor"": { ""id"": 1, ""username"": ""some-user"" } }","[""Introspection""]",[] introspection:messagesdebugview,introspection,messagesdebugview,/-/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.","[""Introspection""]",[] getting_started:getting-started,getting_started,getting-started,Getting started,,[],[] internals:internals,internals,internals,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.,[],[] internals:internals-multiparams,internals,internals-multiparams,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.","[""Internals for plugins""]",[] internals:internals-response,internals,internals-response,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.","[""Internals for plugins""]",[] internals:internals-response-asgi-send,internals,internals-response-asgi-send,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)","[""Internals for plugins"", ""Response class""]",[] internals:internals-response-set-cookie,internals,internals-response-set-cookie,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","[""Internals for plugins"", ""Response class""]",[] internals:internals-datasette,internals,internals-datasette,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","[""Internals for plugins""]",[] internals:datasette-databases,internals,datasette-databases,.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.","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-permissions,internals,datasette-permissions,.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 .","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-plugin-config,internals,datasette-plugin-config,".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 .","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-actors-from-ids,internals,datasette-actors-from-ids,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""} }","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-permission-allowed,internals,datasette-permission-allowed,"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.","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-ensure-permissions,internals,datasette-ensure-permissions,"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"", ], )","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-check-visibility,internals,datasette-check-visibility,"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"", ], )","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-create-token,internals,datasette-create-token,".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"") } }, )","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-get-permission,internals,datasette-get-permission,.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.","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-get-database,internals,datasette-get-database,.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.","[""Internals for plugins"", ""Datasette class""]",[] internals:id1,internals,id1,.get_internal_database(),Returns a database object for reading and writing to the private internal database .,"[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-add-database,internals,datasette-add-database,".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)"" )","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-add-memory-database,internals,datasette-add-memory-database,.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 .","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-remove-database,internals,datasette-remove-database,.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.","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-track-event,internals,datasette-track-event,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""}) )","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-unsign,internals,datasette-unsign,".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.","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-add-message,internals,datasette-add-message,".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.","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-absolute-url,internals,datasette-absolute-url,".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.","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-setting,internals,datasette-setting,.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"")","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-resolve-database,internals,datasette-resolve-database,.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.","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-resolve-table,internals,datasette-resolve-table,.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.","[""Internals for plugins"", ""Datasette class""]",[] internals:datasette-resolve-row,internals,datasette-resolve-row,.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.","[""Internals for plugins"", ""Datasette class""]",[] internals:internals-datasette-urls,internals,internals-datasette-urls,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: Homepage Fixtures database facetable table pragma_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.","[""Internals for plugins"", ""Datasette class""]",[] internals:internals-database,internals,internals-database,Database class,"Instances of the Database class can be used to execute queries against attached SQLite databases, and to run introspection against their schemas.","[""Internals for plugins""]",[] internals:database-constructor,internals,database-constructor,"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.","[""Internals for plugins"", ""Database class""]",[] internals:database-hash,internals,database-hash,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 .","[""Internals for plugins"", ""Database class""]",[] internals:database-execute,internals,database-execute,"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 .","[""Internals for plugins"", ""Database class""]",[] internals:database-execute-fn,internals,database-execute-fn,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)","[""Internals for plugins"", ""Database class""]",[] internals:database-execute-write,internals,database-execute-write,"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.","[""Internals for plugins"", ""Database class""]",[] internals:database-execute-write-fn,internals,database-execute-write-fn,"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.","[""Internals for plugins"", ""Database class""]",[] internals:database-close,internals,database-close,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.","[""Internals for plugins"", ""Database class""]",[] internals:internals-database-introspection,internals,internals-database-introspection,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"", } ] }","[""Internals for plugins"", ""Database class""]",[] internals:internals-internal,internals,internals-internal,Datasette's internal database,"Datasette maintains an ""internal"" SQLite database used for configuration, caching, and storage. Plugins can store configuration, settings, and other data inside this database. By default, Datasette will use a temporary in-memory SQLite database as the internal database, which is created at startup and destroyed at shutdown. Users of Datasette can optionally pass in a --internal flag to specify the path to a SQLite database to use as the internal database, which will persist internal data across Datasette instances. Datasette maintains tables called catalog_databases , catalog_tables , catalog_columns , catalog_indexes , catalog_foreign_keys with details of the attached databases and their schemas. These tables should not be considered a stable API - they may change between Datasette releases. The internal database is not exposed in the Datasette application by default, which means private data can safely be stored without worry of accidentally leaking information through the default Datasette interface and API. However, other plugins do have full read and write access to the internal database. Plugins can access this database by calling internal_db = datasette.get_internal_database() and then executing queries using the Database API . Plugin authors are asked to practice good etiquette when using the internal database, as all plugins use the same database to store data. For example: Use a unique prefix when creating tables, indices, and triggers in the internal database. If your plugin is called datasette-xyz , then prefix names with datasette_xyz_* . Avoid long-running write statements that may stall or block other plugins that are trying to write at the same time. Use temporary tables or shared in-memory attached databases when possible. Avoid implementing features that could expose private data stored in the internal database by other plugins.","[""Internals for plugins""]",[] internals:internals-utils-parse-metadata,internals,internals-utils-parse-metadata,parse_metadata(content),"This function accepts a string containing either JSON or YAML, expected to be of the format described in Metadata . It returns a nested Python dictionary representing the parsed data from that string. If the metadata cannot be parsed as either JSON or YAML the function will raise a utils.BadMetadataError exception. datasette.utils. parse_metadata content : str dict Detects if content is JSON or YAML and parses it appropriately.","[""Internals for plugins"", ""The datasette.utils module""]",[] internals:internals-utils-derive-named-parameters,internals,internals-utils-derive-named-parameters,"derive_named_parameters(db, sql)","Derive the list of named parameters referenced in a SQL query, using an explain query executed against the provided database. async datasette.utils. derive_named_parameters db : Database sql : str List [ str ] Given a SQL statement, return a list of named parameters that are used in the statement e.g. for select * from foo where id=:id this would return [""id""]","[""Internals for plugins"", ""The datasette.utils module""]",[] internals:internals-shortcuts,internals,internals-shortcuts,Import shortcuts,"The following commonly used symbols can be imported directly from the datasette module: from datasette import Response from datasette import Forbidden from datasette import NotFound from datasette import hookimpl from datasette import actor_matches_allow","[""Internals for plugins""]",[] authentication:authentication,authentication,authentication,Authentication and permissions,"Datasette doesn't require authentication by default. Any visitor to a Datasette instance can explore the full data and execute read-only SQL queries. Datasette's plugin system can be used to add many different styles of authentication, such as user accounts, single sign-on or API keys.",[],[] authentication:authentication-actor,authentication,authentication-actor,Actors,"Through plugins, Datasette can support both authenticated users (with cookies) and authenticated API agents (via authentication tokens). The word ""actor"" is used to cover both of these cases. Every request to Datasette has an associated actor value, available in the code as request.actor . This can be None for unauthenticated requests, or a JSON compatible Python dictionary for authenticated users or API agents. The actor dictionary can be any shape - the design of that data structure is left up to the plugins. A useful convention is to include an ""id"" string, as demonstrated by the ""root"" actor below. Plugins can use the actor_from_request(datasette, request) hook to implement custom logic for authenticating an actor based on the incoming HTTP request.","[""Authentication and permissions""]",[] authentication:authentication-permissions-explained,authentication,authentication-permissions-explained,How permissions are resolved,"The datasette.permission_allowed(actor, action, resource=None, default=...) method is called to check if an actor is allowed to perform a specific action. This method asks every plugin that implements the permission_allowed(datasette, actor, action, resource) hook if the actor is allowed to perform the action. Each plugin can return True to indicate that the actor is allowed to perform the action, False if they are not allowed and None if the plugin has no opinion on the matter. False acts as a veto - if any plugin returns False then the permission check is denied. Otherwise, if any plugin returns True then the permission check is allowed. The resource argument can be used to specify a specific resource that the action is being performed against. Some permissions, such as view-instance , do not involve a resource. Others such as view-database have a resource that is a string naming the database. Permissions that take both a database name and the name of a table, view or canned query within that database use a resource that is a tuple of two strings, (database_name, resource_name) . Plugins that implement the permission_allowed() hook can decide if they are going to consider the provided resource or not.","[""Authentication and permissions"", ""Permissions""]",[] authentication:authentication-permissions-config,authentication,authentication-permissions-config,Access permissions in ,"There are two ways to configure permissions using datasette.yaml (or datasette.json ). For simple visibility permissions you can use ""allow"" blocks in the root, database, table and query sections. For other permissions you can use a ""permissions"" block, described in the next section . You can limit who is allowed to view different parts of your Datasette instance using ""allow"" keys in your Configuration . You can control the following: Access to the entire Datasette instance Access to specific databases Access to specific tables and views Access to specific Canned queries If a user cannot access a specific database, they will not be able to access tables, views or queries within that database. If a user cannot access the instance they will not be able to access any of the databases, tables, views or queries.","[""Authentication and permissions""]",[] authentication:authentication-permissions-database,authentication,authentication-permissions-database,Access to specific databases,"To limit access to a specific private.db database to just authenticated users, use the ""allow"" block like this: [[[cog config_example(cog, """""" databases: private: allow: id: ""*"" """""") ]]] [[[end]]]","[""Authentication and permissions"", ""Access permissions in ""]",[] authentication:authentication-permissions-query,authentication,authentication-permissions-query,Access to specific canned queries,"Canned queries allow you to configure named SQL queries in your datasette.yaml that can be executed by users. These queries can be set up to both read and write to the database, so controlling who can execute them can be important. To limit access to the add_name canned query in your dogs.db database to just the root user : [[[cog config_example(cog, """""" databases: dogs: queries: add_name: sql: INSERT INTO names (name) VALUES (:name) write: true allow: id: - root """""") ]]] [[[end]]]","[""Authentication and permissions"", ""Access permissions in ""]",[] authentication:authentication-permissions-other,authentication,authentication-permissions-other,Other permissions in ,"For all other permissions, you can use one or more ""permissions"" blocks in your datasette.yaml configuration file. To grant access to the permissions debug tool to all signed in users, you can grant permissions-debug to any actor with an id matching the wildcard * by adding this a the root of your configuration: [[[cog config_example(cog, """""" permissions: debug-menu: id: '*' """""") ]]] [[[end]]] To grant create-table to the user with id of editor for the docs database: [[[cog config_example(cog, """""" databases: docs: permissions: create-table: id: editor """""") ]]] [[[end]]] And for insert-row against the reports table in that docs database: [[[cog config_example(cog, """""" databases: docs: tables: reports: permissions: insert-row: id: editor """""") ]]] [[[end]]] The permissions debug tool can be useful for helping test permissions that you have configured in this way.","[""Authentication and permissions""]",[] authentication:createtokenview,authentication,createtokenview,API Tokens,"Datasette includes a default mechanism for generating API tokens that can be used to authenticate requests. Authenticated users can create new API tokens using a form on the /-/create-token page. Tokens created in this way can be further restricted to only allow access to specific actions, or to limit those actions to specific databases, tables or queries. Created tokens can then be passed in the Authorization: Bearer $token header of HTTP requests to Datasette. A token created by a user will include that user's ""id"" in the token payload, so any permissions granted to that user based on their ID can be made available to the token as well. When one of these a token accompanies a request, the actor for that request will have the following shape: { ""id"": ""user_id"", ""token"": ""dstok"", ""token_expires"": 1667717426 } The ""id"" field duplicates the ID of the actor who first created the token. The ""token"" field identifies that this actor was authenticated using a Datasette signed token ( dstok ). The ""token_expires"" field, if present, indicates that the token will expire after that integer timestamp. The /-/create-token page cannot be accessed by actors that are authenticated with a ""token"": ""some-value"" property. This is to prevent API tokens from being used to create more tokens. Datasette plugins that implement their own form of API token authentication should follow this convention. You can disable the signed token feature entirely using the allow_signed_tokens setting.","[""Authentication and permissions""]",[] authentication:authentication-cli-create-token,authentication,authentication-cli-create-token,datasette create-token,"You can also create tokens on the command line using the datasette create-token command. This command takes one required argument - the ID of the actor to be associated with the created token. You can specify a -e/--expires-after option in seconds. If omitted, the token will never expire. The command will sign the token using the DATASETTE_SECRET environment variable, if available. You can also pass the secret using the --secret option. This means you can run the command locally to create tokens for use with a deployed Datasette instance, provided you know that instance's secret. To create a token for the root actor that will expire in one hour: datasette create-token root --expires-after 3600 To create a token that never expires using a specific secret: datasette create-token root --secret my-secret-goes-here","[""Authentication and permissions"", ""API Tokens""]",[] authentication:authentication-cli-create-token-restrict,authentication,authentication-cli-create-token-restrict,Restricting the actions that a token can perform,"Tokens created using datasette create-token ACTOR_ID will inherit all of the permissions of the actor that they are associated with. You can pass additional options to create tokens that are restricted to a subset of that actor's permissions. To restrict the token to just specific permissions against all available databases, use the --all option: datasette create-token root --all insert-row --all update-row This option can be passed as many times as you like. In the above example the token will only be allowed to insert and update rows. You can also restrict permissions such that they can only be used within specific databases: datasette create-token root --database mydatabase insert-row The resulting token will only be able to insert rows, and only to tables in the mydatabase database. Finally, you can restrict permissions to individual resources - tables, SQL views and named queries - within a specific database: datasette create-token root --resource mydatabase mytable insert-row These options have short versions: -a for --all , -d for --database and -r for --resource . You can add --debug to see a JSON representation of the token that has been created. Here's a full example: datasette create-token root \ --secret mysecret \ --all view-instance \ --all view-table \ --database docs view-query \ --resource docs documents insert-row \ --resource docs documents update-row \ --debug This example outputs the following: dstok_.eJxFizEKgDAMRe_y5w4qYrFXERGxDkVsMI0uxbubdjFL8l_ez1jhwEQCA6Fjjxp90qtkuHawzdjYrh8MFobLxZ_wBH0_gtnAF-hpS5VfmF8D_lnd97lHqUJgLd6sls4H1qwlhA.nH_7RecYHj5qSzvjhMU95iy0Xlc Decoded: { ""a"": ""root"", ""token"": ""dstok"", ""t"": 1670907246, ""_r"": { ""a"": [ ""vi"", ""vt"" ], ""d"": { ""docs"": [ ""vq"" ] }, ""r"": { ""docs"": { ""documents"": [ ""ir"", ""ur"" ] } } } }","[""Authentication and permissions"", ""API Tokens"", ""datasette create-token""]",[] authentication:permissions-plugins,authentication,permissions-plugins,Checking permissions in plugins,"Datasette plugins can check if an actor has permission to perform an action using the datasette.permission_allowed(...) method. Datasette core performs a number of permission checks, documented below . Plugins can implement the permission_allowed(datasette, actor, action, resource) plugin hook to participate in decisions about whether an actor should be able to perform a specified action.","[""Authentication and permissions""]",[] authentication:authentication-actor-matches-allow,authentication,authentication-actor-matches-allow,actor_matches_allow(),"Plugins that wish to implement this same ""allow"" block permissions scheme can take advantage of the datasette.utils.actor_matches_allow(actor, allow) function: from datasette.utils import actor_matches_allow actor_matches_allow({""id"": ""root""}, {""id"": ""*""}) # returns True The currently authenticated actor is made available to plugins as request.actor .","[""Authentication and permissions""]",[] authentication:permissionsdebugview,authentication,permissionsdebugview,The permissions debug tool,"The debug tool at /-/permissions is only available to the authenticated root user (or any actor granted the permissions-debug action). It shows the thirty most recent permission checks that have been carried out by the Datasette instance. It also provides an interface for running hypothetical permission checks against a hypothetical actor. This is a useful way of confirming that your configured permissions work in the way you expect. This is designed to help administrators and plugin authors understand exactly how permission checks are being carried out, in order to effectively configure Datasette's permission system.","[""Authentication and permissions""]",[] authentication:authentication-ds-actor,authentication,authentication-ds-actor,The ds_actor cookie,"Datasette includes a default authentication plugin which looks for a signed ds_actor cookie containing a JSON actor dictionary. This is how the root actor mechanism works. Authentication plugins can set signed ds_actor cookies themselves like so: response = Response.redirect(""/"") response.set_cookie( ""ds_actor"", datasette.sign({""a"": {""id"": ""cleopaws""}}, ""actor""), ) Note that you need to pass ""actor"" as the namespace to .sign(value, namespace=""default"") . The shape of data encoded in the cookie is as follows: { ""a"": {... actor ...} }","[""Authentication and permissions""]",[] authentication:authentication-ds-actor-expiry,authentication,authentication-ds-actor-expiry,Including an expiry time,"ds_actor cookies can optionally include a signed expiry timestamp, after which the cookies will no longer be valid. Authentication plugins may chose to use this mechanism to limit the lifetime of the cookie. For example, if a plugin implements single-sign-on against another source it may decide to set short-lived cookies so that if the user is removed from the SSO system their existing Datasette cookies will stop working shortly afterwards. To include an expiry, add a ""e"" key to the cookie value containing a base62-encoded integer representing the timestamp when the cookie should expire. For example, here's how to set a cookie that expires after 24 hours: import time from datasette.utils import baseconv expires_at = int(time.time()) + (24 * 60 * 60) response = Response.redirect(""/"") response.set_cookie( ""ds_actor"", datasette.sign( { ""a"": {""id"": ""cleopaws""}, ""e"": baseconv.base62.encode(expires_at), }, ""actor"", ), ) The resulting cookie will encode data that looks something like this: { ""a"": { ""id"": ""cleopaws"" }, ""e"": ""1jjSji"" }","[""Authentication and permissions"", ""The ds_actor cookie""]",[] authentication:logoutview,authentication,logoutview,The /-/logout page,The page at /-/logout provides the ability to log out of a ds_actor cookie authentication session.,"[""Authentication and permissions"", ""The ds_actor cookie""]",[] authentication:id1,authentication,id1,Built-in permissions,"This section lists all of the permission checks that are carried out by Datasette core, along with the resource if it was passed.","[""Authentication and permissions""]",[] authentication:permissions-insert-row,authentication,permissions-insert-row,insert-row,"Actor is allowed to insert rows into a table. resource - tuple: (string, string) The name of the database, then the name of the table Default deny .","[""Authentication and permissions"", ""Built-in permissions""]",[] authentication:permissions-delete-row,authentication,permissions-delete-row,delete-row,"Actor is allowed to delete rows from a table. resource - tuple: (string, string) The name of the database, then the name of the table Default deny .","[""Authentication and permissions"", ""Built-in permissions""]",[] authentication:permissions-update-row,authentication,permissions-update-row,update-row,"Actor is allowed to update rows in a table. resource - tuple: (string, string) The name of the database, then the name of the table Default deny .","[""Authentication and permissions"", ""Built-in permissions""]",[] authentication:permissions-create-table,authentication,permissions-create-table,create-table,"Actor is allowed to create a database table. resource - string The name of the database Default deny .","[""Authentication and permissions"", ""Built-in permissions""]",[] authentication:permissions-alter-table,authentication,permissions-alter-table,alter-table,"Actor is allowed to alter a database table. resource - tuple: (string, string) The name of the database, then the name of the table Default deny .","[""Authentication and permissions"", ""Built-in permissions""]",[] authentication:permissions-drop-table,authentication,permissions-drop-table,drop-table,"Actor is allowed to drop a database table. resource - tuple: (string, string) The name of the database, then the name of the table Default deny .","[""Authentication and permissions"", ""Built-in permissions""]",[] authentication:permissions-permissions-debug,authentication,permissions-permissions-debug,permissions-debug,"Actor is allowed to view the /-/permissions debug page. Default deny .","[""Authentication and permissions"", ""Built-in permissions""]",[] authentication:permissions-debug-menu,authentication,permissions-debug-menu,debug-menu,"Controls if the various debug pages are displayed in the navigation menu. Default deny .","[""Authentication and permissions"", ""Built-in permissions""]",[] publish:publishing,publish,publishing,Publishing data,Datasette includes tools for publishing and deploying your data to the internet. The datasette publish command will deploy a new Datasette instance containing your databases directly to a Heroku or Google Cloud hosting account. You can also use datasette package to create a Docker image that bundles your databases together with the datasette application that is used to serve them.,[],[] pages:pages,pages,pages,Pages and API endpoints,"The Datasette web application offers a number of different pages that can be accessed to explore the data in question, each of which is accompanied by an equivalent JSON API.",[],[] pages:databaseview-hidden,pages,databaseview-hidden,Hidden tables,"Some tables listed on the database page are treated as hidden. Hidden tables are not completely invisible - they can be accessed through the ""hidden tables"" link at the bottom of the page. They are hidden because they represent low-level implementation details which are generally not useful to end-users of Datasette. The following tables are hidden by default: Any table with a name that starts with an underscore - this is a Datasette convention to help plugins easily hide their own internal tables. Tables that have been configured as ""hidden"": true using Hiding tables . *_fts tables that implement SQLite full-text search indexes. Tables relating to the inner workings of the SpatiaLite SQLite extension. sqlite_stat tables used to store statistics used by the query optimizer.","[""Pages and API endpoints"", ""Database""]",[] plugins:plugins-installing,plugins,plugins-installing,Installing plugins,"If a plugin has been packaged for distribution using setuptools you can use the plugin by installing it alongside Datasette in the same virtual environment or Docker container. You can install plugins using the datasette install command: datasette install datasette-vega You can uninstall plugins with datasette uninstall : datasette uninstall datasette-vega You can upgrade plugins with datasette install --upgrade or datasette install -U : datasette install -U datasette-vega This command can also be used to upgrade Datasette itself to the latest released version: datasette install -U datasette You can install multiple plugins at once by listing them as lines in a requirements.txt file like this: datasette-vega datasette-cluster-map Then pass that file to datasette install -r : datasette install -r requirements.txt The install and uninstall commands are thin wrappers around pip install and pip uninstall , which ensure that they run pip in the same virtual environment as Datasette itself.","[""Plugins""]",[] plugins:one-off-plugins-using-plugins-dir,plugins,one-off-plugins-using-plugins-dir,One-off plugins using --plugins-dir,"You can also define one-off per-project plugins by saving them as plugin_name.py functions in a plugins/ folder and then passing that folder to datasette using the --plugins-dir option: datasette mydb.db --plugins-dir=plugins/","[""Plugins"", ""Installing plugins""]",[] plugins:deploying-plugins-using-datasette-publish,plugins,deploying-plugins-using-datasette-publish,Deploying plugins using datasette publish,"The datasette publish and datasette package commands both take an optional --install argument. You can use this one or more times to tell Datasette to pip install specific plugins as part of the process: datasette publish cloudrun mydb.db --install=datasette-vega You can use the name of a package on PyPI or any of the other valid arguments to pip install such as a URL to a .zip file: datasette publish cloudrun mydb.db \ --install=https://url-to-my-package.zip","[""Plugins"", ""Installing plugins""]",[] plugins:plugins-datasette-load-plugins,plugins,plugins-datasette-load-plugins,Controlling which plugins are loaded,"Datasette defaults to loading every plugin that is installed in the same virtual environment as Datasette itself. You can set the DATASETTE_LOAD_PLUGINS environment variable to a comma-separated list of plugin names to load a controlled subset of plugins instead. For example, to load just the datasette-vega and datasette-cluster-map plugins, set DATASETTE_LOAD_PLUGINS to datasette-vega,datasette-cluster-map : export DATASETTE_LOAD_PLUGINS='datasette-vega,datasette-cluster-map' datasette mydb.db Or: DATASETTE_LOAD_PLUGINS='datasette-vega,datasette-cluster-map' \ datasette mydb.db To disable the loading of all additional plugins, set DATASETTE_LOAD_PLUGINS to an empty string: export DATASETTE_LOAD_PLUGINS='' datasette mydb.db A quick way to test this setting is to use it with the datasette plugins command: DATASETTE_LOAD_PLUGINS='datasette-vega' datasette plugins This should output the following: [ { ""name"": ""datasette-vega"", ""static"": true, ""templates"": false, ""version"": ""0.6.2"", ""hooks"": [ ""extra_css_urls"", ""extra_js_urls"" ] } ]","[""Plugins""]",[] plugins:plugins-configuration,plugins,plugins-configuration,Plugin configuration,"Plugins can have their own configuration, embedded in a configuration file . Configuration options for plugins live within a ""plugins"" key in that file, which can be included at the root, database or table level. Here is an example of some plugin configuration for a specific table: [[[cog from metadata_doc import config_example config_example(cog, { ""databases"": { ""sf-trees"": { ""tables"": { ""Street_Tree_List"": { ""plugins"": { ""datasette-cluster-map"": { ""latitude_column"": ""lat"", ""longitude_column"": ""lng"" } } } } } } }) ]]] [[[end]]] This tells the datasette-cluster-map column which latitude and longitude columns should be used for a table called Street_Tree_List inside a database file called sf-trees.db .","[""Plugins""]",[] plugins:plugins-configuration-secret,plugins,plugins-configuration-secret,Secret configuration values,"Some plugins may need configuration that should stay secret - API keys for example. There are two ways in which you can store secret configuration values. As environment variables . If your secret lives in an environment variable that is available to the Datasette process, you can indicate that the configuration value should be read from that environment variable like so: [[[cog config_example(cog, { ""plugins"": { ""datasette-auth-github"": { ""client_secret"": { ""$env"": ""GITHUB_CLIENT_SECRET"" } } } }) ]]] [[[end]]] As values in separate files . Your secrets can also live in files on disk. To specify a secret should be read from a file, provide the full file path like this: [[[cog config_example(cog, { ""plugins"": { ""datasette-auth-github"": { ""client_secret"": { ""$file"": ""/secrets/client-secret"" } } } }) ]]] [[[end]]] If you are publishing your data using the datasette publish family of commands, you can use the --plugin-secret option to set these secrets at publish time. For example, using Heroku you might run the following command: datasette publish heroku my_database.db \ --name my-heroku-app-demo \ --install=datasette-auth-github \ --plugin-secret datasette-auth-github client_id your_client_id \ --plugin-secret datasette-auth-github client_secret your_client_secret This will set the necessary environment variables and add the following to the deployed metadata.yaml : [[[cog config_example(cog, { ""plugins"": { ""datasette-auth-github"": { ""client_id"": { ""$env"": ""DATASETTE_AUTH_GITHUB_CLIENT_ID"" }, ""client_secret"": { ""$env"": ""DATASETTE_AUTH_GITHUB_CLIENT_SECRET"" } } } }) ]]] [[[end]]]","[""Plugins"", ""Plugin configuration""]",[] json_api:id1,json_api,id1,JSON API,"Datasette provides a JSON API for your SQLite databases. Anything you can do through the Datasette user interface can also be accessed as JSON via the API. To access the API for a page, either click on the .json link on that page or edit the URL and add a .json extension to it.",[],[] json_api:json-api-default,json_api,json-api-default,Default representation,"The default JSON representation of data from a SQLite table or custom query looks like this: { ""ok"": true, ""rows"": [ { ""id"": 3, ""name"": ""Detroit"" }, { ""id"": 2, ""name"": ""Los Angeles"" }, { ""id"": 4, ""name"": ""Memnonia"" }, { ""id"": 1, ""name"": ""San Francisco"" } ], ""truncated"": false } ""ok"" is always true if an error did not occur. The ""rows"" key is a list of objects, each one representing a row. The ""truncated"" key lets you know if the query was truncated. This can happen if a SQL query returns more than 1,000 results (or the max_returned_rows setting). For table pages, an additional key ""next"" may be present. This indicates that the next page in the pagination set can be retrieved using ?_next=VALUE .","[""JSON API""]",[] json_api:json-api-shapes,json_api,json-api-shapes,Different shapes,"The _shape parameter can be used to access alternative formats for the rows key which may be more convenient for your application. There are three options: ?_shape=objects - ""rows"" is a list of JSON key/value objects - the default ?_shape=arrays - ""rows"" is a list of lists, where the order of values in each list matches the order of the columns ?_shape=array - a JSON array of objects - effectively just the ""rows"" key from the default representation ?_shape=array&_nl=on - a newline-separated list of JSON objects ?_shape=arrayfirst - a flat JSON array containing just the first value from each row ?_shape=object - a JSON object keyed using the primary keys of the rows _shape=arrays looks like this: { ""ok"": true, ""next"": null, ""rows"": [ [3, ""Detroit""], [2, ""Los Angeles""], [4, ""Memnonia""], [1, ""San Francisco""] ] } _shape=array looks like this: [ { ""id"": 3, ""name"": ""Detroit"" }, { ""id"": 2, ""name"": ""Los Angeles"" }, { ""id"": 4, ""name"": ""Memnonia"" }, { ""id"": 1, ""name"": ""San Francisco"" } ] _shape=array&_nl=on looks like this: {""id"": 1, ""value"": ""Myoporum laetum :: Myoporum""} {""id"": 2, ""value"": ""Metrosideros excelsa :: New Zealand Xmas Tree""} {""id"": 3, ""value"": ""Pinus radiata :: Monterey Pine""} _shape=arrayfirst looks like this: [1, 2, 3] _shape=object looks like this: { ""1"": { ""id"": 1, ""value"": ""Myoporum laetum :: Myoporum"" }, ""2"": { ""id"": 2, ""value"": ""Metrosideros excelsa :: New Zealand Xmas Tree"" }, ""3"": { ""id"": 3, ""value"": ""Pinus radiata :: Monterey Pine"" } ] The object shape is only available for queries against tables - custom SQL queries and views do not have an obvious primary key so cannot be returned using this format. The object keys are always strings. If your table has a compound primary key, the object keys will be a comma-separated string.","[""JSON API""]",[] json_api:id2,json_api,id2,Table arguments,The Datasette table view takes a number of special query string arguments.,"[""JSON API""]",[] json_api:column-filter-arguments,json_api,column-filter-arguments,Column filter arguments,"You can filter the data returned by the table based on column values using a query string argument. ?column__exact=value or ?_column=value Returns rows where the specified column exactly matches the value. ?column__not=value Returns rows where the column does not match the value. ?column__contains=value Rows where the string column contains the specified value ( column like ""%value%"" in SQL). ?column__notcontains=value Rows where the string column does not contain the specified value ( column not like ""%value%"" in SQL). ?column__endswith=value Rows where the string column ends with the specified value ( column like ""%value"" in SQL). ?column__startswith=value Rows where the string column starts with the specified value ( column like ""value%"" in SQL). ?column__gt=value Rows which are greater than the specified value. ?column__gte=value Rows which are greater than or equal to the specified value. ?column__lt=value Rows which are less than the specified value. ?column__lte=value Rows which are less than or equal to the specified value. ?column__like=value Match rows with a LIKE clause, case insensitive and with % as the wildcard character. ?column__notlike=value Match rows that do not match the provided LIKE clause. ?column__glob=value Similar to LIKE but uses Unix wildcard syntax and is case sensitive. ?column__in=value1,value2,value3 Rows where column matches any of the provided values. You can use a comma separated string, or you can use a JSON array. The JSON array option is useful if one of your matching values itself contains a comma: ?column__in=[""value"",""value,with,commas""] ?column__notin=value1,value2,value3 Rows where column does not match any of the provided values. The inverse of __in= . Also supports JSON arrays. ?column__arraycontains=value Works against columns that contain JSON arrays - matches if any of the values in that array match the provided value. This is only available if the json1 SQLite extension is enabled. ?column__arraynotcontains=value Works against columns that contain JSON arrays - matches if none of the values in that array match the provided value. This is only available if the json1 SQLite extension is enabled. ?column__date=value Column is a datestamp occurring on the specified YYYY-MM-DD date, e.g. 2018-01-02 . ?column__isnull=1 Matches rows where the column is null. ?column__notnull=1 Matches rows where the column is not null. ?column__isblank=1 Matches rows where the column is blank, meaning null or the empty string. ?column__notblank=1 Matches rows where the column is not blank.","[""JSON API"", ""Table arguments""]",[] json_api:expand-foreign-keys,json_api,expand-foreign-keys,Expanding foreign key references,"Datasette can detect foreign key relationships and resolve those references into labels. The HTML interface does this by default for every detected foreign key column - you can turn that off using ?_labels=off . You can request foreign keys be expanded in JSON using the _labels=on or _label=COLUMN special query string parameters. Here's what an expanded row looks like: [ { ""rowid"": 1, ""TreeID"": 141565, ""qLegalStatus"": { ""value"": 1, ""label"": ""Permitted Site"" }, ""qSpecies"": { ""value"": 1, ""label"": ""Myoporum laetum :: Myoporum"" }, ""qAddress"": ""501X Baker St"", ""SiteOrder"": 1 } ] The column in the foreign key table that is used for the label can be specified in metadata.json - see Specifying the label column for a table .","[""JSON API""]",[] json_api:json-api-discover-alternate,json_api,json-api-discover-alternate,Discovering the JSON for a page,"Most of the HTML pages served by Datasette provide a mechanism for discovering their JSON equivalents using the HTML link mechanism. You can find this near the top of the source code of those pages, looking like this: The JSON URL is also made available in a Link HTTP header for the page: Link: https://latest.datasette.io/fixtures/sortable.json; rel=""alternate""; type=""application/json+datasette""","[""JSON API""]",[] json_api:json-api-cors,json_api,json-api-cors,Enabling CORS,"If you start Datasette with the --cors option, each JSON endpoint will be served with the following additional HTTP headers: [[[cog from datasette.utils import add_cors_headers import textwrap headers = {} add_cors_headers(headers) output = ""\n"".join(""{}: {}"".format(k, v) for k, v in headers.items()) cog.out(""\n::\n\n"") cog.out(textwrap.indent(output, ' ')) cog.out(""\n\n"") ]]] Access-Control-Allow-Origin: * Access-Control-Allow-Headers: Authorization, Content-Type Access-Control-Expose-Headers: Link Access-Control-Allow-Methods: GET, POST, HEAD, OPTIONS Access-Control-Max-Age: 3600 [[[end]]] This allows JavaScript running on any domain to make cross-origin requests to interact with the Datasette API. If you start Datasette without the --cors option only JavaScript running on the same domain as Datasette will be able to access the API. Here's how to serve data.db with CORS enabled: datasette data.db --cors","[""JSON API""]",[] json_api:json-api-write,json_api,json-api-write,The JSON write API,"Datasette provides a write API for JSON data. This is a POST-only API that requires an authenticated API token, see API Tokens . The token will need to have the specified Permissions .","[""JSON API""]",[] json_api:tableinsertview,json_api,tableinsertview,Inserting rows,"This requires the insert-row permission. A single row can be inserted using the ""row"" key: POST ///-/insert Content-Type: application/json Authorization: Bearer dstok_ { ""row"": { ""column1"": ""value1"", ""column2"": ""value2"" } } If successful, this will return a 201 status code and the newly inserted row, for example: { ""rows"": [ { ""id"": 1, ""column1"": ""value1"", ""column2"": ""value2"" } ] } To insert multiple rows at a time, use the same API method but send a list of dictionaries as the ""rows"" key: POST //
/-/insert Content-Type: application/json Authorization: Bearer dstok_ { ""rows"": [ { ""column1"": ""value1"", ""column2"": ""value2"" }, { ""column1"": ""value3"", ""column2"": ""value4"" } ] } If successful, this will return a 201 status code and a {""ok"": true} response body. The maximum number rows that can be submitted at once defaults to 100, but this can be changed using the max_insert_rows setting. To return the newly inserted rows, add the ""return"": true key to the request body: { ""rows"": [ { ""column1"": ""value1"", ""column2"": ""value2"" }, { ""column1"": ""value3"", ""column2"": ""value4"" } ], ""return"": true } This will return the same ""rows"" key as the single row example above. There is a small performance penalty for using this option. If any of your rows have a primary key that is already in use, you will get an error and none of the rows will be inserted: { ""ok"": false, ""errors"": [ ""UNIQUE constraint failed: new_table.id"" ] } Pass ""ignore"": true to ignore these errors and insert the other rows: { ""rows"": [ { ""id"": 1, ""column1"": ""value1"", ""column2"": ""value2"" }, { ""id"": 2, ""column1"": ""value3"", ""column2"": ""value4"" } ], ""ignore"": true } Or you can pass ""replace"": true to replace any rows with conflicting primary keys with the new values. This requires the update-row permission. Pass ""alter: true to automatically add any missing columns to the table. This requires the alter-table permission.","[""JSON API"", ""The JSON write API""]",[] json_api:tableupsertview,json_api,tableupsertview,Upserting rows,"An upsert is an insert or update operation. If a row with a matching primary key already exists it will be updated - otherwise a new row will be inserted. The upsert API is mostly the same shape as the insert API . It requires both the insert-row and update-row permissions. POST //
/-/upsert Content-Type: application/json Authorization: Bearer dstok_ { ""rows"": [ { ""id"": 1, ""title"": ""Updated title for 1"", ""description"": ""Updated description for 1"" }, { ""id"": 2, ""description"": ""Updated description for 2"", }, { ""id"": 3, ""title"": ""Item 3"", ""description"": ""Description for 3"" } ] } Imagine a table with a primary key of id and which already has rows with id values of 1 and 2 . The above example will: Update the row with id of 1 to set both title and description to the new values Update the row with id of 2 to set title to the new value - description will be left unchanged Insert a new row with id of 3 and both title and description set to the new values Similar to /-/insert , a row key with an object can be used instead of a rows array to upsert a single row. If successful, this will return a 200 status code and a {""ok"": true} response body. Add ""return"": true to the request body to return full copies of the affected rows after they have been inserted or updated: { ""rows"": [ { ""id"": 1, ""title"": ""Updated title for 1"", ""description"": ""Updated description for 1"" }, { ""id"": 2, ""description"": ""Updated description for 2"", }, { ""id"": 3, ""title"": ""Item 3"", ""description"": ""Description for 3"" } ], ""return"": true } This will return the following: { ""ok"": true, ""rows"": [ { ""id"": 1, ""title"": ""Updated title for 1"", ""description"": ""Updated description for 1"" }, { ""id"": 2, ""title"": ""Item 2"", ""description"": ""Updated description for 2"" }, { ""id"": 3, ""title"": ""Item 3"", ""description"": ""Description for 3"" } ] } When using upsert you must provide the primary key column (or columns if the table has a compound primary key) for every row, or you will get a 400 error: { ""ok"": false, ""errors"": [ ""Row 0 is missing primary key column(s): \""id\"""" ] } If your table does not have an explicit primary key you should pass the SQLite rowid key instead. Pass ""alter: true to automatically add any missing columns to the table. This requires the alter-table permission.","[""JSON API"", ""The JSON write API""]",[] json_api:rowupdateview,json_api,rowupdateview,Updating a row,"To update a row, make a POST to //
//-/update . This requires the update-row permission. POST //
//-/update Content-Type: application/json Authorization: Bearer dstok_ { ""update"": { ""text_column"": ""New text string"", ""integer_column"": 3, ""float_column"": 3.14 } } here is the tilde-encoded primary key value of the row to update - or a comma-separated list of primary key values if the table has a composite primary key. You only need to pass the columns you want to update. Any other columns will be left unchanged. If successful, this will return a 200 status code and a {""ok"": true} response body. Add ""return"": true to the request body to return the updated row: { ""update"": { ""title"": ""New title"" }, ""return"": true } The returned JSON will look like this: { ""ok"": true, ""row"": { ""id"": 1, ""title"": ""New title"", ""other_column"": ""Will be present here too"" } } Any errors will return {""errors"": [""... descriptive message ...""], ""ok"": false} , and a 400 status code for a bad input or a 403 status code for an authentication or permission error. Pass ""alter: true to automatically add any missing columns to the table. This requires the alter-table permission.","[""JSON API"", ""The JSON write API""]",[] json_api:rowdeleteview,json_api,rowdeleteview,Deleting a row,"To delete a row, make a POST to //
//-/delete . This requires the delete-row permission. POST //
//-/delete Content-Type: application/json Authorization: Bearer dstok_ here is the tilde-encoded primary key value of the row to delete - or a comma-separated list of primary key values if the table has a composite primary key. If successful, this will return a 200 status code and a {""ok"": true} response body. Any errors will return {""errors"": [""... descriptive message ...""], ""ok"": false} , and a 400 status code for a bad input or a 403 status code for an authentication or permission error.","[""JSON API"", ""The JSON write API""]",[] json_api:tablecreateview,json_api,tablecreateview,Creating a table,"To create a table, make a POST to //-/create . This requires the create-table permission. POST //-/create Content-Type: application/json Authorization: Bearer dstok_ { ""table"": ""name_of_new_table"", ""columns"": [ { ""name"": ""id"", ""type"": ""integer"" }, { ""name"": ""title"", ""type"": ""text"" } ], ""pk"": ""id"" } The JSON here describes the table that will be created: table is the name of the table to create. This field is required. columns is a list of columns to create. Each column is a dictionary with name and type keys. name is the name of the column. This is required. type is the type of the column. This is optional - if not provided, text will be assumed. The valid types are text , integer , float and blob . pk is the primary key for the table. This is optional - if not provided, Datasette will create a SQLite table with a hidden rowid column. If the primary key is an integer column, it will be configured to automatically increment for each new record. If you set this to id without including an id column in the list of columns , Datasette will create an auto-incrementing integer ID column for you. pks can be used instead of pk to create a compound primary key. It should be a JSON list of column names to use in that primary key. ignore can be set to true to ignore existing rows by primary key if the table already exists. replace can be set to true to replace existing rows by primary key if the table already exists. This requires the update-row permission. alter can be set to true if you want to automatically add any missing columns to the table. This requires the alter-table permission. If the table is successfully created this will return a 201 status code and the following response: { ""ok"": true, ""database"": ""data"", ""table"": ""name_of_new_table"", ""table_url"": ""http://127.0.0.1:8001/data/name_of_new_table"", ""table_api_url"": ""http://127.0.0.1:8001/data/name_of_new_table.json"", ""schema"": ""CREATE TABLE [name_of_new_table] (\n [id] INTEGER PRIMARY KEY,\n [title] TEXT\n)"" }","[""JSON API"", ""The JSON write API""]",[] json_api:tablecreateview-example,json_api,tablecreateview-example,Creating a table from example data,"Instead of specifying columns directly you can instead pass a single example row or a list of rows . Datasette will create a table with a schema that matches those rows and insert them for you: POST //-/create Content-Type: application/json Authorization: Bearer dstok_ { ""table"": ""creatures"", ""rows"": [ { ""id"": 1, ""name"": ""Tarantula"" }, { ""id"": 2, ""name"": ""Kākāpō"" } ], ""pk"": ""id"" } Doing this requires both the create-table and insert-row permissions. The 201 response here will be similar to the columns form, but will also include the number of rows that were inserted as row_count : { ""ok"": true, ""database"": ""data"", ""table"": ""creatures"", ""table_url"": ""http://127.0.0.1:8001/data/creatures"", ""table_api_url"": ""http://127.0.0.1:8001/data/creatures.json"", ""schema"": ""CREATE TABLE [creatures] (\n [id] INTEGER PRIMARY KEY,\n [name] TEXT\n)"", ""row_count"": 2 } You can call the create endpoint multiple times for the same table provided you are specifying the table using the rows or row option. New rows will be inserted into the table each time. This means you can use this API if you are unsure if the relevant table has been created yet. If you pass a row to the create endpoint with a primary key that already exists you will get an error that looks like this: { ""ok"": false, ""errors"": [ ""UNIQUE constraint failed: creatures.id"" ] } You can avoid this error by passing the same ""ignore"": true or ""replace"": true options to the create endpoint as you can to the insert endpoint . To use the ""replace"": true option you will also need the update-row permission. Pass ""alter"": true to automatically add any missing columns to the existing table that are present in the rows you are submitting. This requires the alter-table permission.","[""JSON API"", ""The JSON write API""]",[] json_api:tabledropview,json_api,tabledropview,Dropping tables,"To drop a table, make a POST to //
/-/drop . This requires the drop-table permission. POST //
/-/drop Content-Type: application/json Authorization: Bearer dstok_ Without a POST body this will return a status 200 with a note about how many rows will be deleted: { ""ok"": true, ""database"": """", ""table"": ""
"", ""row_count"": 5, ""message"": ""Pass \""confirm\"": true to confirm"" } If you pass the following POST body: { ""confirm"": true } Then the table will be dropped and a status 200 response of {""ok"": true} will be returned. Any errors will return {""errors"": [""... descriptive message ...""], ""ok"": false} , and a 400 status code for a bad input or a 403 status code for an authentication or permission error.","[""JSON API"", ""The JSON write API""]",[] csv_export:csv-export-url-parameters,csv_export,csv-export-url-parameters,URL parameters,"The following options can be used to customize the CSVs returned by Datasette. ?_header=off This removes the first row of the CSV file specifying the headings - only the row data will be returned. ?_stream=on Stream all matching records, not just the first page of results. See below. ?_dl=on Causes Datasette to return a content-disposition: attachment; filename=""filename.csv"" header.","[""CSV export""]",[] csv_export:streaming-all-records,csv_export,streaming-all-records,Streaming all records,"The stream all rows option is designed to be as efficient as possible - under the hood it takes advantage of Python 3 asyncio capabilities and Datasette's efficient pagination to stream back the full CSV file. Since databases can get pretty large, by default this option is capped at 100MB - if a table returns more than 100MB of data the last line of the CSV will be a truncation error message. You can increase or remove this limit using the max_csv_mb config setting. You can also disable the CSV export feature entirely using allow_csv_stream .","[""CSV export""]",[] settings:id1,settings,id1,Settings,,[],[] settings:using-setting,settings,using-setting,Using --setting,"Datasette supports a number of settings. These can be set using the --setting name value option to datasette serve . You can set multiple settings at once like this: datasette mydatabase.db \ --setting default_page_size 50 \ --setting sql_time_limit_ms 3500 \ --setting max_returned_rows 2000 Settings can also be specified in the database.yaml configuration file .","[""Settings""]",[] settings:config-dir,settings,config-dir,Configuration directory mode,"Normally you configure Datasette using command-line options. For a Datasette instance with custom templates, custom plugins, a static directory and several databases this can get quite verbose: datasette one.db two.db \ --metadata=metadata.json \ --template-dir=templates/ \ --plugins-dir=plugins \ --static css:css As an alternative to this, you can run Datasette in configuration directory mode. Create a directory with the following structure: # In a directory called my-app: my-app/one.db my-app/two.db my-app/datasette.yaml my-app/metadata.json my-app/templates/index.html my-app/plugins/my_plugin.py my-app/static/my.css Now start Datasette by providing the path to that directory: datasette my-app/ Datasette will detect the files in that directory and automatically configure itself using them. It will serve all *.db files that it finds, will load metadata.json if it exists, and will load the templates , plugins and static folders if they are present. The files that can be included in this directory are as follows. All are optional. *.db (or *.sqlite3 or *.sqlite ) - SQLite database files that will be served by Datasette datasette.yaml - Configuration for the Datasette instance metadata.json - Metadata for those databases - metadata.yaml or metadata.yml can be used as well inspect-data.json - the result of running datasette inspect *.db --inspect-file=inspect-data.json from the configuration directory - any database files listed here will be treated as immutable, so they should not be changed while Datasette is running templates/ - a directory containing Custom templates plugins/ - a directory containing plugins, see Writing one-off plugins static/ - a directory containing static files - these will be served from /static/filename.txt , see Serving static files","[""Settings""]",[] settings:id2,settings,id2,Settings,"The following options can be set using --setting name value , or by storing them in the settings.json file for use with Configuration directory mode .","[""Settings""]",[] settings:setting-default-allow-sql,settings,setting-default-allow-sql,default_allow_sql,"Should users be able to execute arbitrary SQL queries by default? Setting this to off causes permission checks for execute-sql to fail by default. datasette mydatabase.db --setting default_allow_sql off Another way to achieve this is to add ""allow_sql"": false to your datasette.yaml file, as described in Controlling the ability to execute arbitrary SQL . This setting offers a more convenient way to do this.","[""Settings"", ""Settings""]",[] settings:setting-default-page-size,settings,setting-default-page-size,default_page_size,"The default number of rows returned by the table page. You can over-ride this on a per-page basis using the ?_size=80 query string parameter, provided you do not specify a value higher than the max_returned_rows setting. You can set this default using --setting like so: datasette mydatabase.db --setting default_page_size 50","[""Settings"", ""Settings""]",[] settings:setting-sql-time-limit-ms,settings,setting-sql-time-limit-ms,sql_time_limit_ms,"By default, queries have a time limit of one second. If a query takes longer than this to run Datasette will terminate the query and return an error. If this time limit is too short for you, you can customize it using the sql_time_limit_ms limit - for example, to increase it to 3.5 seconds: datasette mydatabase.db --setting sql_time_limit_ms 3500 You can optionally set a lower time limit for an individual query using the ?_timelimit=100 query string argument: /my-database/my-table?qSpecies=44&_timelimit=100 This would set the time limit to 100ms for that specific query. This feature is useful if you are working with databases of unknown size and complexity - a query that might make perfect sense for a smaller table could take too long to execute on a table with millions of rows. By setting custom time limits you can execute queries ""optimistically"" - e.g. give me an exact count of rows matching this query but only if it takes less than 100ms to calculate.","[""Settings"", ""Settings""]",[] settings:setting-max-returned-rows,settings,setting-max-returned-rows,max_returned_rows,"Datasette returns a maximum of 1,000 rows of data at a time. If you execute a query that returns more than 1,000 rows, Datasette will return the first 1,000 and include a warning that the result set has been truncated. You can use OFFSET/LIMIT or other methods in your SQL to implement pagination if you need to return more than 1,000 rows. You can increase or decrease this limit like so: datasette mydatabase.db --setting max_returned_rows 2000","[""Settings"", ""Settings""]",[] settings:setting-max-insert-rows,settings,setting-max-insert-rows,max_insert_rows,"Maximum rows that can be inserted at a time using the bulk insert API, see Inserting rows . Defaults to 100. You can increase or decrease this limit like so: datasette mydatabase.db --setting max_insert_rows 1000","[""Settings"", ""Settings""]",[] settings:setting-allow-facet,settings,setting-allow-facet,allow_facet,"Allow users to specify columns they would like to facet on using the ?_facet=COLNAME URL parameter to the table view. This is enabled by default. If disabled, facets will still be displayed if they have been specifically enabled in metadata.json configuration for the table. Here's how to disable this feature: datasette mydatabase.db --setting allow_facet off","[""Settings"", ""Settings""]",[] settings:setting-default-facet-size,settings,setting-default-facet-size,default_facet_size,"The default number of unique rows returned by Facets is 30. You can customize it like this: datasette mydatabase.db --setting default_facet_size 50","[""Settings"", ""Settings""]",[] settings:setting-facet-time-limit-ms,settings,setting-facet-time-limit-ms,facet_time_limit_ms,"This is the time limit Datasette allows for calculating a facet, which defaults to 200ms: datasette mydatabase.db --setting facet_time_limit_ms 1000","[""Settings"", ""Settings""]",[] settings:setting-facet-suggest-time-limit-ms,settings,setting-facet-suggest-time-limit-ms,facet_suggest_time_limit_ms,"When Datasette calculates suggested facets it needs to run a SQL query for every column in your table. The default for this time limit is 50ms to account for the fact that it needs to run once for every column. If the time limit is exceeded the column will not be suggested as a facet. You can increase this time limit like so: datasette mydatabase.db --setting facet_suggest_time_limit_ms 500","[""Settings"", ""Settings""]",[] settings:setting-suggest-facets,settings,setting-suggest-facets,suggest_facets,"Should Datasette calculate suggested facets? On by default, turn this off like so: datasette mydatabase.db --setting suggest_facets off","[""Settings"", ""Settings""]",[] settings:setting-allow-download,settings,setting-allow-download,allow_download,"Should users be able to download the original SQLite database using a link on the database index page? This is turned on by default. However, databases can only be downloaded if they are served in immutable mode and not in-memory. If downloading is unavailable for either of these reasons, the download link is hidden even if allow_download is on. To disable database downloads, use the following: datasette mydatabase.db --setting allow_download off","[""Settings"", ""Settings""]",[] settings:setting-allow-signed-tokens,settings,setting-allow-signed-tokens,allow_signed_tokens,"Should users be able to create signed API tokens to access Datasette? This is turned on by default. Use the following to turn it off: datasette mydatabase.db --setting allow_signed_tokens off Turning this setting off will disable the /-/create-token page, described here . It will also cause any incoming Authorization: Bearer dstok_... API tokens to be ignored.","[""Settings"", ""Settings""]",[] settings:setting-max-signed-tokens-ttl,settings,setting-max-signed-tokens-ttl,max_signed_tokens_ttl,"Maximum allowed expiry time for signed API tokens created by users. Defaults to 0 which means no limit - tokens can be created that will never expire. Set this to a value in seconds to limit the maximum expiry time. For example, to set that limit to 24 hours you would use: datasette mydatabase.db --setting max_signed_tokens_ttl 86400 This setting is enforced when incoming tokens are processed.","[""Settings"", ""Settings""]",[] settings:setting-default-cache-ttl,settings,setting-default-cache-ttl,default_cache_ttl,"Default HTTP caching max-age header in seconds, used for Cache-Control: max-age=X . Can be over-ridden on a per-request basis using the ?_ttl= query string parameter. Set this to 0 to disable HTTP caching entirely. Defaults to 5 seconds. datasette mydatabase.db --setting default_cache_ttl 60","[""Settings"", ""Settings""]",[] settings:setting-allow-csv-stream,settings,setting-allow-csv-stream,allow_csv_stream,"Enables the CSV export feature where an entire table (potentially hundreds of thousands of rows) can be exported as a single CSV file. This is turned on by default - you can turn it off like this: datasette mydatabase.db --setting allow_csv_stream off","[""Settings"", ""Settings""]",[] settings:setting-max-csv-mb,settings,setting-max-csv-mb,max_csv_mb,"The maximum size of CSV that can be exported, in megabytes. Defaults to 100MB. You can disable the limit entirely by settings this to 0: datasette mydatabase.db --setting max_csv_mb 0","[""Settings"", ""Settings""]",[] settings:setting-truncate-cells-html,settings,setting-truncate-cells-html,truncate_cells_html,"In the HTML table view, truncate any strings that are longer than this value. The full value will still be available in CSV, JSON and on the individual row HTML page. Set this to 0 to disable truncation. datasette mydatabase.db --setting truncate_cells_html 0","[""Settings"", ""Settings""]",[] settings:setting-force-https-urls,settings,setting-force-https-urls,force_https_urls,"Forces self-referential URLs in the JSON output to always use the https:// protocol. This is useful for cases where the application itself is hosted using HTTP but is served to the outside world via a proxy that enables HTTPS. datasette mydatabase.db --setting force_https_urls 1","[""Settings"", ""Settings""]",[] settings:setting-base-url,settings,setting-base-url,base_url,"If you are running Datasette behind a proxy, it may be useful to change the root path used for the Datasette instance. For example, if you are sending traffic from https://www.example.com/tools/datasette/ through to a proxied Datasette instance you may wish Datasette to use /tools/datasette/ as its root URL. You can do that like so: datasette mydatabase.db --setting base_url /tools/datasette/","[""Settings"", ""Settings""]",[] settings:setting-secret,settings,setting-secret,Configuring the secret,"Datasette uses a secret string to sign secure values such as cookies. If you do not provide a secret, Datasette will create one when it starts up. This secret will reset every time the Datasette server restarts though, so things like authentication cookies and API tokens will not stay valid between restarts. You can pass a secret to Datasette in two ways: with the --secret command-line option or by setting a DATASETTE_SECRET environment variable. datasette mydb.db --secret=SECRET_VALUE_HERE Or: export DATASETTE_SECRET=SECRET_VALUE_HERE datasette mydb.db One way to generate a secure random secret is to use Python like this: python3 -c 'import secrets; print(secrets.token_hex(32))' cdb19e94283a20f9d42cca50c5a4871c0aa07392db308755d60a1a5b9bb0fa52 Plugin authors make use of this signing mechanism in their plugins using .sign(value, namespace=""default"") and .unsign(value, namespace=""default"") .","[""Settings""]",[] settings:setting-publish-secrets,settings,setting-publish-secrets,Using secrets with datasette publish,"The datasette publish and datasette package commands both generate a secret for you automatically when Datasette is deployed. This means that every time you deploy a new version of a Datasette project, a new secret will be generated. This will cause signed cookies to become invalid on every fresh deploy. You can fix this by creating a secret that will be used for multiple deploys and passing it using the --secret option: datasette publish cloudrun mydb.db --service=my-service --secret=cdb19e94283a20f9d42cca5","[""Settings""]",[] changelog:id1,changelog,id1,Changelog,,[],[] changelog:v1-0-a9,changelog,v1-0-a9,1.0a9 (2024-02-16),This alpha release adds basic alter table support to the Datasette Write API and fixes a permissions bug relating to the /upsert API endpoint.,"[""Changelog""]",[] changelog:id21,changelog,id21,0.60 (2022-01-13),,"[""Changelog""]",[] changelog:id44,changelog,id44,0.51.1 (2020-10-31),Improvements to the new Binary data documentation page.,"[""Changelog""]",[] changelog:id45,changelog,id45,0.51 (2020-10-31),"A new visual design, plugin hooks for adding navigation options, better handling of binary data, URL building utility methods and better support for running Datasette behind a proxy.","[""Changelog""]",[] changelog:signed-values-and-secrets,changelog,signed-values-and-secrets,Signed values and secrets,"Both flash messages and user authentication needed a way to sign values and set signed cookies. Two new methods are now available for plugins to take advantage of this mechanism: .sign(value, namespace=""default"") and .unsign(value, namespace=""default"") . Datasette will generate a secret automatically when it starts up, but to avoid resetting the secret (and hence invalidating any cookies) every time the server restarts you should set your own secret. You can pass a secret to Datasette using the new --secret option or with a DATASETTE_SECRET environment variable. See Configuring the secret for more details. You can also set a secret when you deploy Datasette using datasette publish or datasette package - see Using secrets with datasette publish . Plugins can now sign values and verify their signatures using the datasette.sign() and datasette.unsign() methods.","[""Changelog"", ""0.44 (2020-06-11)""]",[] changelog:cookie-methods,changelog,cookie-methods,Cookie methods,"Plugins can now use the new response.set_cookie() method to set cookies. A new request.cookies method on the :ref:internals_request` can be used to read incoming cookies.","[""Changelog"", ""0.44 (2020-06-11)""]",[] changelog:id84,changelog,id84,0.29 (2019-07-07),"ASGI, new plugin hooks, facet by date and much, much more...","[""Changelog""]",[] changelog:v0-29-medium-changes,changelog,v0-29-medium-changes,Easier custom templates for table rows,"If you want to customize the display of individual table rows, you can do so using a _table.html template include that looks something like this: {% for row in display_rows %}

{{ row[""title""] }}

{{ row[""description""] }}

Category: {{ row.display(""category_id"") }}

{% endfor %} This is a backwards incompatible change . If you previously had a custom template called _rows_and_columns.html you need to rename it to _table.html . See Custom templates for full details.","[""Changelog"", ""0.29 (2019-07-07)""]",[] changelog:id87,changelog,id87,0.27.1 (2019-05-09),"Tiny bugfix release: don't install tests/ in the wrong place. Thanks, Veit Heller.","[""Changelog""]",[] changelog:id91,changelog,id91,0.25.2 (2018-12-16),"datasette publish heroku now uses the python-3.6.7 runtime Added documentation on how to build the documentation Added documentation covering our release process Upgraded to pytest 4.0.2","[""Changelog""]",[] changelog:foreign-key-expansions,changelog,foreign-key-expansions,Foreign key expansions,"When Datasette detects a foreign key reference it attempts to resolve a label for that reference (automatically or using the Specifying the label column for a table metadata option) so it can display a link to the associated row. This expansion is now also available for JSON and CSV representations of the table, using the new _labels=on query string option. See Expanding foreign key references for more details.","[""Changelog"", ""0.23 (2018-06-18)""]",[] changelog:id154,changelog,id154,0.17 (2018-04-13),Release 0.17 to fix issues with PyPI,"[""Changelog""]",[] changelog:id208,changelog,id208,0.11 (2017-11-14),"Added datasette publish now --force option. This calls now with --force - useful as it means you get a fresh copy of datasette even if Now has already cached that docker layer. Enable --cors by default when running in a container.","[""Changelog""]",[] changelog:id211,changelog,id211,0.9 (2017-11-13),"Added --sql_time_limit_ms and --extra-options . The serve command now accepts --sql_time_limit_ms for customizing the SQL time limit. The publish and package commands now accept --extra-options which can be used to specify additional options to be passed to the datasite serve command when it executes inside the resulting Docker containers.","[""Changelog""]",[] index:contents,index,contents,Contents,"Getting started Play with a live demo Follow a tutorial Datasette in your browser with Datasette Lite Try Datasette without installing anything using Glitch Using Datasette on your own computer Installation Basic installation Datasette Desktop for Mac Using Homebrew Using pip Advanced installation options Using pipx Using Docker A note about extensions Configuration Configuration via the command-line datasette.yaml reference Settings Plugin configuration Permissions configuration Canned queries configuration Custom CSS and JavaScript The Datasette Ecosystem sqlite-utils Dogsheep CLI reference datasette --help datasette serve datasette --get datasette serve --help-settings datasette plugins datasette install datasette uninstall datasette publish datasette publish cloudrun datasette publish heroku datasette package datasette inspect datasette create-token Pages and API endpoints Top-level index Database Hidden tables Table Row Publishing data datasette publish Publishing to Google Cloud Run Publishing to Heroku Publishing to Vercel Publishing to Fly Custom metadata and plugins datasette package Deploying Datasette Deployment fundamentals Running Datasette using systemd Running Datasette using OpenRC Deploying using buildpacks Running Datasette behind a proxy Nginx proxy configuration Apache proxy configuration JSON API Default representation Different shapes Pagination Special JSON arguments Table arguments Column filter arguments Special table arguments Expanding foreign key references Discovering the JSON for a page Enabling CORS The JSON write API Inserting rows Upserting rows Updating a row Deleting a row Creating a table Creating a table from example data Dropping tables Running SQL queries Named parameters Views Canned queries Canned query parameters Additional canned query options Writable canned queries Magic parameters JSON API for writable canned queries Pagination Cross-database queries Authentication and permissions Actors Using the ""root"" actor Permissions How permissions are resolved Defining permissions with ""allow"" blocks The /-/allow-debug tool Access permissions in datasette.yaml Access to an instance Access to specific databases Access to specific tables and views Access to specific canned queries Controlling the ability to execute arbitrary SQL Other permissions in datasette.yaml API Tokens datasette create-token Checking permissions in plugins actor_matches_allow() The permissions debug tool The ds_actor cookie Including an expiry time The /-/logout page Built-in permissions view-instance view-database view-database-download view-table view-query insert-row delete-row update-row create-table alter-table drop-table execute-sql permissions-debug debug-menu Performance and caching Immutable mode Using ""datasette inspect"" HTTP caching datasette-hashed-urls CSV export URL parameters Streaming all records Binary data Linking to binary downloads Binary plugins Facets Facets in query strings Facets in metadata Suggested facets Speeding up facets with indexes Facet by JSON array Facet by date Full-text search The table page and table view API Advanced SQLite search queries Configuring full-text search for a table or view Searches using custom SQL Enabling full-text search for a SQLite table Configuring FTS using sqlite-utils Configuring FTS using csvs-to-sqlite Configuring FTS by hand FTS versions SpatiaLite Warning Installation Installing SpatiaLite on OS X Installing SpatiaLite on Linux Spatial indexing latitude/longitude columns Making use of a spatial index Importing shapefiles into SpatiaLite Importing GeoJSON polygons using Shapely Querying polygons using within() Metadata Per-database and per-table metadata Source, license and about Column descriptions Specifying units for a column Setting a default sort order Setting a custom page size Setting which columns can be used for sorting Specifying the label column for a table Hiding tables Metadata reference Top-level metadata Database-level metadata Table-level metadata Settings Using --setting Configuration directory mode Settings default_allow_sql default_page_size sql_time_limit_ms max_returned_rows max_insert_rows num_sql_threads allow_facet default_facet_size facet_time_limit_ms facet_suggest_time_limit_ms suggest_facets allow_download allow_signed_tokens max_signed_tokens_ttl default_cache_ttl cache_size_kb allow_csv_stream max_csv_mb truncate_cells_html force_https_urls template_debug trace_debug base_url Configuring the secret Using secrets with datasette publish Introspection /-/metadata /-/versions /-/plugins /-/settings /-/config /-/databases /-/threads /-/actor /-/messages Custom pages and templates CSS classes on the Serving static files Publishing static assets Custom templates Custom pages Path parameters for pages Custom headers and status codes Returning 404s Custom redirects Custom error pages Plugins Installing plugins One-off plugins using --plugins-dir Deploying plugins using datasette publish Controlling which plugins are loaded Seeing what plugins are installed Plugin configuration Secret configuration values Writing plugins Tracing plugin hooks Writing one-off plugins Starting an installable plugin using cookiecutter Packaging a plugin Static assets Custom templates Writing plugins that accept configuration Designing URLs for your plugin Building URLs within plugins Plugins that define new plugin hooks JavaScript plugins The datasette_init event datasetteManager JavaScript plugin objects makeAboveTablePanelConfigs() makeColumnActions(columnDetails) Selectors 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) Testing plugins Setting up a Datasette test instance Using datasette.client in tests Using pdb for errors thrown inside Datasette Using pytest fixtures Testing outbound HTTP calls with pytest-httpx Registering a plugin for the duration of a test Internals for plugins Request object The MultiParams class Response class Returning a response with .asgi_send(send) Setting cookies with response.set_cookie() Datasette class .databases .permissions .plugin_config(plugin_name, database=None, table=None) await .render_template(template, context=None, request=None) await .actors_from_ids(actor_ids) await .permission_allowed(actor, action, resource=None, default=...) await .ensure_permissions(actor, permissions) await .check_visibility(actor, action=None, resource=None, permissions=None) .create_token(actor_id, expires_after=None, restrict_all=None, restrict_database=None, restrict_resource=None) .get_permission(name_or_abbr) .get_database(name) .get_internal_database() .add_database(db, name=None, route=None) .add_memory_database(name) .remove_database(name) await .track_event(event) .sign(value, namespace=""default"") .unsign(value, namespace=""default"") .add_message(request, message, type=datasette.INFO) .absolute_url(request, path) .setting(key) .resolve_database(request) .resolve_table(request) .resolve_row(request) datasette.client datasette.urls Database class Database(ds, path=None, is_mutable=True, is_memory=False, memory_name=None) db.hash await db.execute(sql, ...) Results await db.execute_fn(fn) await db.execute_write(sql, params=None, block=True) await db.execute_write_script(sql, block=True) await db.execute_write_many(sql, params_seq, block=True) await db.execute_write_fn(fn, block=True, transaction=True) await db.execute_isolated_fn(fn) db.close() Database introspection CSRF protection Datasette's internal database The datasette.utils module parse_metadata(content) await_me_maybe(value) derive_named_parameters(db, sql) Tilde encoding datasette.tracer Tracing child tasks Import shortcuts Events LoginEvent LogoutEvent CreateTokenEvent CreateTableEvent DropTableEvent AlterTableEvent InsertRowsEvent UpsertRowsEvent UpdateRowEvent DeleteRowEvent Contributing General guidelines Setting up a development environment Running the tests Using fixtures Debugging Code formatting Running Black blacken-docs Prettier Editing and building the documentation Running Cog Continuously deployed demo instances Release process Alpha and beta releases Releasing bug fixes from a branch Upgrading CodeMirror Changelog 1.0a13 (2024-03-12) 1.0a12 (2024-02-29) 1.0a11 (2024-02-19) 1.0a10 (2024-02-17) 1.0a9 (2024-02-16) Alter table support for create, insert, upsert and update Permissions fix for the upsert API Permission checks now consider opinions from every plugin Other changes 1.0a8 (2024-02-07) Configuration JavaScript plugins Plugin hooks Documentation Minor fixes 0.64.6 (2023-12-22) 0.64.5 (2023-10-08) 1.0a7 (2023-09-21) 0.64.4 (2023-09-21) 1.0a6 (2023-09-07) 1.0a5 (2023-08-29) 1.0a4 (2023-08-21) 1.0a3 (2023-08-09) Smaller changes 0.64.2 (2023-03-08) 0.64.1 (2023-01-11) 0.64 (2023-01-09) 0.63.3 (2022-12-17) 1.0a2 (2022-12-14) 1.0a1 (2022-12-01) 1.0a0 (2022-11-29) Signed API tokens Write API 0.63.2 (2022-11-18) 0.63.1 (2022-11-10) 0.63 (2022-10-27) Features Plugin hooks and internals Documentation 0.62 (2022-08-14) Features Plugin hooks Bug fixes Documentation 0.61.1 (2022-03-23) 0.61 (2022-03-23) 0.60.2 (2022-02-07) 0.60.1 (2022-01-20) 0.60 (2022-01-13) Plugins and internals Faceting Other small fixes 0.59.4 (2021-11-29) 0.59.3 (2021-11-20) 0.59.2 (2021-11-13) 0.59.1 (2021-10-24) 0.59 (2021-10-14) 0.58.1 (2021-07-16) 0.58 (2021-07-14) 0.57.1 (2021-06-08) 0.57 (2021-06-05) New features Bug fixes and other improvements 0.56.1 (2021-06-05) 0.56 (2021-03-28) 0.55 (2021-02-18) 0.54.1 (2021-02-02) 0.54 (2021-01-25) The _internal database Named in-memory database support JavaScript modules Code formatting with Black and Prettier Other changes 0.53 (2020-12-10) 0.52.5 (2020-12-09) 0.52.4 (2020-12-05) 0.52.3 (2020-12-03) 0.52.2 (2020-12-02) 0.52.1 (2020-11-29) 0.52 (2020-11-28) 0.51.1 (2020-10-31) 0.51 (2020-10-31) New visual design Plugins can now add links within Datasette Binary data URL building Running Datasette behind a proxy Smaller changes 0.50.2 (2020-10-09) 0.50.1 (2020-10-09) 0.50 (2020-10-09) 0.49.1 (2020-09-15) 0.49 (2020-09-14) 0.48 (2020-08-16) 0.47.3 (2020-08-15) 0.47.2 (2020-08-12) 0.47.1 (2020-08-11) 0.47 (2020-08-11) 0.46 (2020-08-09) 0.45 (2020-07-01) Magic parameters for canned queries Log out Better plugin documentation New plugin hooks Smaller changes 0.44 (2020-06-11) Authentication Permissions Writable canned queries Flash messages Signed values and secrets CSRF protection Cookie methods register_routes() plugin hooks Smaller changes The road to Datasette 1.0 0.43 (2020-05-28) 0.42 (2020-05-08) 0.41 (2020-05-06) 0.40 (2020-04-21) 0.39 (2020-03-24) 0.38 (2020-03-08) 0.37.1 (2020-03-02) 0.37 (2020-02-25) 0.36 (2020-02-21) 0.35 (2020-02-04) 0.34 (2020-01-29) 0.33 (2019-12-22) 0.32 (2019-11-14) 0.31.2 (2019-11-13) 0.31.1 (2019-11-12) 0.31 (2019-11-11) 0.30.2 (2019-11-02) 0.30.1 (2019-10-30) 0.30 (2019-10-18) 0.29.3 (2019-09-02) 0.29.2 (2019-07-13) 0.29.1 (2019-07-11) 0.29 (2019-07-07) ASGI New plugin hook: asgi_wrapper New plugin hook: extra_template_vars Secret plugin configuration options Facet by date Easier custom templates for table rows ?_through= for joins through many-to-many tables Small changes 0.28 (2019-05-19) Supporting databases that change Faceting improvements, and faceting plugins datasette publish cloudrun register_output_renderer plugins Medium changes Small changes 0.27.1 (2019-05-09) 0.27 (2019-01-31) 0.26.1 (2019-01-10) 0.26 (2019-01-02) 0.25.2 (2018-12-16) 0.25.1 (2018-11-04) 0.25 (2018-09-19) 0.24 (2018-07-23) 0.23.2 (2018-07-07) 0.23.1 (2018-06-21) 0.23 (2018-06-18) CSV export Foreign key expansions New configuration settings Control HTTP caching with ?_ttl= Improved support for SpatiaLite latest.datasette.io Miscellaneous 0.22.1 (2018-05-23) 0.22 (2018-05-20) 0.21 (2018-05-05) 0.20 (2018-04-20) 0.19 (2018-04-16) 0.18 (2018-04-14) 0.17 (2018-04-13) 0.16 (2018-04-13) 0.15 (2018-04-09) 0.14 (2017-12-09) 0.13 (2017-11-24) 0.12 (2017-11-16) 0.11 (2017-11-14) 0.10 (2017-11-14) 0.9 (2017-11-13) 0.8 (2017-11-13)","[""Datasette""]",[] configuration:id1,configuration,id1,Configuration,"Datasette offers several ways to configure your Datasette instances: server settings, plugin configuration, authentication, and more. Most configuration can be handled using a datasette.yaml configuration file, passed to datasette using the -c/--config flag: datasette mydatabase.db --config datasette.yaml This file can also use JSON, as datasette.json . YAML is recommended over JSON due to its support for comments and multi-line strings.",[],[] configuration:configuration-reference,configuration,configuration-reference,,"The following example shows some of the valid configuration options that can exist inside datasette.yaml . [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" # Datasette settings block settings: default_page_size: 50 sql_time_limit_ms: 3500 max_returned_rows: 2000 # top-level plugin configuration plugins: datasette-my-plugin: key: valueA # Database and table-level configuration databases: your_db_name: # plugin configuration for the your_db_name database plugins: datasette-my-plugin: key: valueA tables: your_table_name: allow: # Only the root user can access this table id: root # plugin configuration for the your_table_name table # inside your_db_name database plugins: datasette-my-plugin: key: valueB """""") ) ]]] [[[end]]]","[""Configuration""]",[] configuration:configuration-reference-settings,configuration,configuration-reference-settings,Settings,"Settings can be configured in datasette.yaml with the settings key: [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" # inside datasette.yaml settings: default_allow_sql: off default_page_size: 50 """""").strip() ) ]]] [[[end]]] The full list of settings is available in the settings documentation . Settings can also be passed to Datasette using one or more --setting name value command line options.`","[""Configuration"", null]",[] configuration:configuration-reference-plugins,configuration,configuration-reference-plugins,Plugin configuration,"Datasette plugins often require configuration. This plugin configuration should be placed in plugins keys inside datasette.yaml . Most plugins are configured at the top-level of the file, using the plugins key: [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" # inside datasette.yaml plugins: datasette-my-plugin: key: my_value """""").strip() ) ]]] [[[end]]] Some plugins can be configured at the database or table level. These should use a plugins key nested under the appropriate place within the databases object: [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" # inside datasette.yaml databases: my_database: # plugin configuration for the my_database database plugins: datasette-my-plugin: key: my_value my_other_database: tables: my_table: # plugin configuration for the my_table table inside the my_other_database database plugins: datasette-my-plugin: key: my_value """""").strip() ) ]]] [[[end]]]","[""Configuration"", null]",[] configuration:configuration-reference-permissions,configuration,configuration-reference-permissions,Permissions configuration,"Datasette's authentication and permissions system can also be configured using datasette.yaml . Here is a simple example: [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" # Instance is only available to users 'sharon' and 'percy': allow: id: - sharon - percy # Only 'percy' is allowed access to the accounting database: databases: accounting: allow: id: percy """""").strip() ) ]]] [[[end]]] Access permissions in datasette.yaml has the full details.","[""Configuration"", null]",[] configuration:configuration-reference-canned-queries,configuration,configuration-reference-canned-queries,Canned queries configuration,"Canned queries are named SQL queries that appear in the Datasette interface. They can be configured in datasette.yaml using the queries key at the database level: [[[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]]] See the canned queries documentation for more, including how to configure writable canned queries .","[""Configuration"", null]",[] performance:performance,performance,performance,Performance and caching,"Datasette runs on top of SQLite, and SQLite has excellent performance. For small databases almost any query should return in just a few milliseconds, and larger databases (100s of MBs or even GBs of data) should perform extremely well provided your queries make sensible use of database indexes. That said, there are a number of tricks you can use to improve Datasette's performance.",[],[] performance:performance-immutable-mode,performance,performance-immutable-mode,Immutable mode,"If you can be certain that a SQLite database file will not be changed by another process you can tell Datasette to open that file in immutable mode . Doing so will disable all locking and change detection, which can result in improved query performance. This also enables further optimizations relating to HTTP caching, described below. To open a file in immutable mode pass it to the datasette command using the -i option: datasette -i data.db When you open a file in immutable mode like this Datasette will also calculate and cache the row counts for each table in that database when it first starts up, further improving performance.","[""Performance and caching""]",[] performance:performance-inspect,performance,performance-inspect,"Using ""datasette inspect""","Counting the rows in a table can be a very expensive operation on larger databases. In immutable mode Datasette performs this count only once and caches the results, but this can still cause server startup time to increase by several seconds or more. If you know that a database is never going to change you can precalculate the table row counts once and store then in a JSON file, then use that file when you later start the server. To create a JSON file containing the calculated row counts for a database, use the following: datasette inspect data.db --inspect-file=counts.json Then later you can start Datasette against the counts.json file and use it to skip the row counting step and speed up server startup: datasette -i data.db --inspect-file=counts.json You need to use the -i immutable mode against the database file here or the counts from the JSON file will be ignored. You will rarely need to use this optimization in every-day use, but several of the datasette publish commands described in Publishing data use this optimization for better performance when deploying a database file to a hosting provider.","[""Performance and caching""]",[] installation:installing-plugins,installation,installing-plugins,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","[""Installation"", ""Advanced installation options"", ""Using Docker""]","[{""href"": ""http://127.0.0.1:8001/-/plugins"", ""label"": ""http://127.0.0.1:8001/-/plugins""}, {""href"": ""https://datasette.io/plugins/datasette-ripgrep"", ""label"": ""datasette-ripgrep""}]" installation:loading-spatialite,installation,loading-spatialite,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","[""Installation"", ""Advanced installation options"", ""Using Docker""]","[{""href"": ""http://127.0.0.1:8001/-/versions"", ""label"": ""http://127.0.0.1:8001/-/versions""}]" plugin_hooks:plugin-hook-prepare-jinja2-environment,plugin_hooks,plugin-hook-prepare-jinja2-environment,"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","[""Plugin hooks""]","[{""href"": ""http://jinja.pocoo.org/docs/2.10/api/#custom-filters"", ""label"": ""register custom\n template filters""}, {""href"": ""https://datasette.io/plugins/datasette-edit-templates"", ""label"": ""datasette-edit-templates""}]" getting_started:getting-started-your-computer,getting_started,getting-started-your-computer,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"", ... } ] }","[""Getting started""]","[{""href"": ""http://localhost:8001/"", ""label"": ""http://localhost:8001/""}, {""href"": ""http://localhost:8001/History/downloads"", ""label"": ""http://localhost:8001/History/downloads""}, {""href"": ""http://localhost:8001/History/downloads.json"", ""label"": ""http://localhost:8001/History/downloads.json""}, {""href"": ""http://localhost:8001/History/downloads.json?_shape=objects"", ""label"": ""http://localhost:8001/History/downloads.json?_shape=objects""}]" writing_plugins:writing-plugins-one-off,writing_plugins,writing-plugins-one-off,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.","[""Writing plugins""]","[{""href"": ""http://localhost:8001/mydb"", ""label"": ""http://localhost:8001/mydb""}]" changelog:id88,changelog,id88,0.27 (2019-01-31),"New command: datasette plugins ( documentation ) shows you the currently installed list of plugins. Datasette can now output newline-delimited JSON using the new ?_shape=array&_nl=on query string option. Added documentation on The Datasette Ecosystem . Now using Python 3.7.2 as the base for the official Datasette Docker image .","[""Changelog""]","[{""href"": ""http://ndjson.org/"", ""label"": ""newline-delimited JSON""}, {""href"": ""https://hub.docker.com/r/datasetteproject/datasette/"", ""label"": ""Datasette Docker image""}]" changelog:id85,changelog,id85,0.28 (2019-05-19),A salmagundi of new features!,"[""Changelog""]","[{""href"": ""https://adamj.eu/tech/2019/01/18/a-salmagundi-of-django-alpha-announcements/"", ""label"": ""salmagundi""}]" changelog:asgi,changelog,asgi,ASGI,"ASGI is the Asynchronous Server Gateway Interface standard. I've been wanting to convert Datasette into an ASGI application for over a year - Port Datasette to ASGI #272 tracks thirteen months of intermittent development - but with Datasette 0.29 the change is finally released. This also means Datasette now runs on top of Uvicorn and no longer depends on Sanic . I wrote about the significance of this change in Porting Datasette to ASGI, and Turtles all the way down . The most exciting consequence of this change is that Datasette plugins can now take advantage of the ASGI standard.","[""Changelog"", ""0.29 (2019-07-07)""]","[{""href"": ""https://asgi.readthedocs.io/"", ""label"": ""ASGI""}, {""href"": ""https://github.com/simonw/datasette/issues/272"", ""label"": ""Port Datasette to ASGI #272""}, {""href"": ""https://www.uvicorn.org/"", ""label"": ""Uvicorn""}, {""href"": ""https://github.com/huge-success/sanic"", ""label"": ""Sanic""}, {""href"": ""https://simonwillison.net/2019/Jun/23/datasette-asgi/"", ""label"": ""Porting Datasette to ASGI, and Turtles all the way down""}]" plugin_hooks:plugin-asgi-wrapper,plugin_hooks,plugin-asgi-wrapper,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","[""Plugin hooks""]","[{""href"": ""https://asgi.readthedocs.io/"", ""label"": ""ASGI""}, {""href"": ""https://www.starlette.io/middleware/"", ""label"": ""Starlette""}, {""href"": ""https://datasette.io/plugins/datasette-cors"", ""label"": ""datasette-cors""}, {""href"": ""https://datasette.io/plugins/datasette-pyinstrument"", ""label"": ""datasette-pyinstrument""}, {""href"": ""https://datasette.io/plugins/datasette-total-page-time"", ""label"": ""datasette-total-page-time""}]" internals:internals-request,internals,internals-request,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'}}}","[""Internals for plugins""]","[{""href"": ""https://asgi.readthedocs.io/en/latest/specs/www.html#connection-scope"", ""label"": ""ASGI HTTP connection scope""}]" plugin_hooks:plugin-hook-skip-csrf,plugin_hooks,plugin-hook-skip-csrf,"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.","[""Plugin hooks""]","[{""href"": ""https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope"", ""label"": ""ASGI scope""}]" installation:installation-homebrew,installation,installation-homebrew,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","[""Installation"", ""Basic installation""]","[{""href"": ""https://brew.sh/"", ""label"": ""Homebrew""}]" spatialite:installing-spatialite-on-os-x,spatialite,installing-spatialite-on-os-x,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","[""SpatiaLite"", ""Installation""]","[{""href"": ""https://brew.sh/"", ""label"": ""Homebrew""}]" plugin_hooks:plugin-hook-register-commands,plugin_hooks,plugin-hook-register-commands,register_commands(cli),"cli - the root Datasette Click command group Use this to register additional CLI commands Register additional CLI commands that can be run using datsette yourcommand ... . This provides a mechanism by which plugins can add new CLI commands to Datasette. This example registers a new datasette verify file1.db file2.db command that checks if the provided file paths are valid SQLite databases: 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","[""Plugin hooks""]","[{""href"": ""https://click.palletsprojects.com/en/latest/commands/#callback-invocation"", ""label"": ""Click command group""}, {""href"": ""https://click.palletsprojects.com/"", ""label"": ""Click documentation""}, {""href"": ""https://datasette.io/plugins/datasette-auth-passwords"", ""label"": ""datasette-auth-passwords""}, {""href"": ""https://datasette.io/plugins/datasette-verify"", ""label"": ""datasette-verify""}]" publish:publish-cloud-run,publish,publish-cloud-run,Publishing to Google Cloud Run,"Google Cloud Run allows you to publish data in a scale-to-zero environment, so your application will start running when the first request is received and will shut down again when traffic ceases. This means you only pay for time spent serving traffic. Cloud Run is a great option for inexpensively hosting small, low traffic projects - but costs can add up for projects that serve a lot of requests. Be particularly careful if your project has tables with large numbers of rows. Search engine crawlers that index a page for every row could result in a high bill. The datasette-block-robots plugin can be used to request search engine crawlers omit crawling your site, which can help avoid this issue. You will first need to install and configure the Google Cloud CLI tools by following these instructions . You can then publish one or more SQLite database files to Google Cloud Run using the following command: datasette publish cloudrun mydatabase.db --service=my-database A Cloud Run service is a single hosted application. The service name you specify will be used as part of the Cloud Run URL. If you deploy to a service name that you have used in the past your new deployment will replace the previous one. If you omit the --service option you will be asked to pick a service name interactively during the deploy. You may need to interact with prompts from the tool. Many of the prompts ask for values that can be set as properties for the Google Cloud SDK if you want to avoid the prompts. For example, the default region for the deployed instance can be set using the command: gcloud config set run/region us-central1 You should replace us-central1 with your desired region . Alternately, you can specify the region by setting the CLOUDSDK_RUN_REGION environment variable. Once it has finished it will output a URL like this one: Service [my-service] revision [my-service-00001] has been deployed and is serving traffic at https://my-service-j7hipcg4aq-uc.a.run.app Cloud Run provides a URL on the .run.app domain, but you can also point your own domain or subdomain at your Cloud Run service - see mapping custom domains in the Cloud Run documentation for details. See datasette publish cloudrun for the full list of options for this command.","[""Publishing data"", ""datasette publish""]","[{""href"": ""https://cloud.google.com/run/"", ""label"": ""Google Cloud Run""}, {""href"": ""https://datasette.io/plugins/datasette-block-robots"", ""label"": ""datasette-block-robots""}, {""href"": ""https://cloud.google.com/sdk/"", ""label"": ""these instructions""}, {""href"": ""https://cloud.google.com/sdk/docs/properties"", ""label"": ""set as properties for the Google Cloud SDK""}, {""href"": ""https://cloud.google.com/about/locations"", ""label"": ""region""}, {""href"": ""https://cloud.google.com/run/docs/mapping-custom-domains"", ""label"": ""mapping custom domains""}]" changelog:v0-28-publish-cloudrun,changelog,v0-28-publish-cloudrun,datasette publish cloudrun,"Google Cloud Run is a brand new serverless hosting platform from Google, which allows you to build a Docker container which will run only when HTTP traffic is received and will shut down (and hence cost you nothing) the rest of the time. It's similar to Zeit's Now v1 Docker hosting platform which sadly is no longer accepting signups from new users. The new datasette publish cloudrun command was contributed by Romain Primet ( #434 ) and publishes selected databases to a new Datasette instance running on Google Cloud Run. See Publishing to Google Cloud Run for full documentation.","[""Changelog"", ""0.28 (2019-05-19)""]","[{""href"": ""https://cloud.google.com/run/"", ""label"": ""Google Cloud Run""}, {""href"": ""https://hyperion.alpha.spectrum.chat/zeit/now/cannot-create-now-v1-deployments~d206a0d4-5835-4af5-bb5c-a17f0171fb25?m=MTU0Njk2NzgwODM3OA=="", ""label"": ""no longer accepting signups""}, {""href"": ""https://github.com/simonw/datasette/pull/434"", ""label"": ""#434""}]" contributing:contributing-upgrading-codemirror,contributing,contributing-upgrading-codemirror,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.","[""Contributing""]","[{""href"": ""https://codemirror.net/"", ""label"": ""CodeMirror""}, {""href"": ""https://latest.datasette.io/fixtures"", ""label"": ""this page""}]" facets:id1,facets,id1,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.",[],"[{""href"": ""https://congress-legislators.datasettes.com/legislators/legislator_terms?_facet=type&_facet=party&_facet=state&_facet_size=10"", ""label"": ""an example""}]" changelog:id23,changelog,id23,0.59.3 (2021-11-20),"Fixed numerous bugs when running Datasette behind a proxy with a prefix URL path using the base_url setting. A live demo of this mode is now available at datasette-apache-proxy-demo.datasette.io/prefix/ . ( #1519 , #838 ) ?column__arraycontains= and ?column__arraynotcontains= table parameters now also work against SQL views. ( #448 ) ?_facet_array=column no longer returns incorrect counts if columns contain the same value more than once.","[""Changelog""]","[{""href"": ""https://datasette-apache-proxy-demo.datasette.io/prefix/"", ""label"": ""datasette-apache-proxy-demo.datasette.io/prefix/""}, {""href"": ""https://github.com/simonw/datasette/issues/1519"", ""label"": ""#1519""}, {""href"": ""https://github.com/simonw/datasette/issues/838"", ""label"": ""#838""}, {""href"": ""https://github.com/simonw/datasette/issues/448"", ""label"": ""#448""}]" ecosystem:ecosystem,ecosystem,ecosystem,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",[],"[{""href"": ""https://datasette.io/"", ""label"": ""Datasette project website""}, {""href"": ""https://datasette.io/plugins"", ""label"": ""Plugins directory on datasette.io""}, {""href"": ""https://datasette.io/tools"", ""label"": ""Tools directory on datasette.io""}]" changelog:id37,changelog,id37,0.53 (2020-12-10),"Datasette has an official project website now, at https://datasette.io/ . This release mainly updates the documentation to reflect the new site. New ?column__arraynotcontains= table filter. ( #1132 ) datasette serve has a new --create option, which will create blank database files if they do not already exist rather than exiting with an error. ( #1135 ) New ?_header=off option for CSV export which omits the CSV header row, documented here . ( #1133 ) ""Powered by Datasette"" link in the footer now links to https://datasette.io/ . ( #1138 ) Project news no longer lives in the README - it can now be found at https://datasette.io/news . ( #1137 )","[""Changelog""]","[{""href"": ""https://datasette.io/"", ""label"": ""https://datasette.io/""}, {""href"": ""https://github.com/simonw/datasette/issues/1132"", ""label"": ""#1132""}, {""href"": ""https://github.com/simonw/datasette/issues/1135"", ""label"": ""#1135""}, {""href"": ""https://github.com/simonw/datasette/issues/1133"", ""label"": ""#1133""}, {""href"": ""https://datasette.io/"", ""label"": ""https://datasette.io/""}, {""href"": ""https://github.com/simonw/datasette/issues/1138"", ""label"": ""#1138""}, {""href"": ""https://datasette.io/news"", ""label"": ""https://datasette.io/news""}, {""href"": ""https://github.com/simonw/datasette/issues/1137"", ""label"": ""#1137""}]" installation:installation-datasette-desktop,installation,installation-datasette-desktop,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.,"[""Installation"", ""Basic installation""]","[{""href"": ""https://datasette.io/desktop"", ""label"": ""Datasette Desktop""}]" writing_plugins:writing-plugins-designing-urls,writing_plugins,writing-plugins-designing-urls,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.","[""Writing plugins""]","[{""href"": ""https://datasette.io/plugins"", ""label"": ""plugins directory""}]" plugin_hooks:plugin-register-output-renderer,plugin_hooks,plugin-register-output-renderer,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","[""Plugin hooks""]","[{""href"": ""https://datasette.io/plugins/datasette-atom"", ""label"": ""datasette-atom""}, {""href"": ""https://datasette.io/plugins/datasette-ics"", ""label"": ""datasette-ics""}, {""href"": ""https://datasette.io/plugins/datasette-geojson"", ""label"": ""datasette-geojson""}, {""href"": ""https://datasette.io/plugins/datasette-copyable"", ""label"": ""datasette-copyable""}]" plugin_hooks:plugin-register-routes,plugin_hooks,plugin-register-routes,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","[""Plugin hooks""]","[{""href"": ""https://datasette.io/plugins/datasette-auth-github"", ""label"": ""datasette-auth-github""}, {""href"": ""https://datasette.io/plugins/datasette-psutil"", ""label"": ""datasette-psutil""}]" changelog:v1-0-a4,changelog,v1-0-a4,1.0a4 (2023-08-21),"This alpha fixes a security issue with the /-/api API explorer. On authenticated Datasette instances (instances protected using plugins such as datasette-auth-passwords ) the API explorer interface could reveal the names of databases and tables within the protected instance. The data stored in those tables was not revealed. For more information and workarounds, read the security advisory . The issue has been present in every previous alpha version of Datasette 1.0: versions 1.0a0, 1.0a1, 1.0a2 and 1.0a3. Also in this alpha: The new datasette plugins --requirements option outputs a list of currently installed plugins in Python requirements.txt format, useful for duplicating that installation elsewhere. ( #2133 ) Writable canned queries can now define a on_success_message_sql field in their configuration, containing a SQL query that should be executed upon successful completion of the write operation in order to generate a message to be shown to the user. ( #2138 ) The automatically generated border color for a database is now shown in more places around the application. ( #2119 ) Every instance of example shell script code in the documentation should now include a working copy button, free from additional syntax. ( #2140 )","[""Changelog""]","[{""href"": ""https://datasette.io/plugins/datasette-auth-passwords"", ""label"": ""datasette-auth-passwords""}, {""href"": ""https://github.com/simonw/datasette/security/advisories/GHSA-7ch3-7pp7-7cpq"", ""label"": ""the security advisory""}, {""href"": ""https://github.com/simonw/datasette/issues/2133"", ""label"": ""#2133""}, {""href"": ""https://github.com/simonw/datasette/issues/2138"", ""label"": ""#2138""}, {""href"": ""https://github.com/simonw/datasette/issues/2119"", ""label"": ""#2119""}, {""href"": ""https://github.com/simonw/datasette/issues/2140"", ""label"": ""#2140""}]" plugin_hooks:plugin-hook-actor-from-request,plugin_hooks,plugin-hook-actor-from-request,"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","[""Plugin hooks""]","[{""href"": ""https://datasette.io/plugins/datasette-auth-tokens"", ""label"": ""datasette-auth-tokens""}, {""href"": ""https://datasette.io/plugins/datasette-auth-passwords"", ""label"": ""datasette-auth-passwords""}]" configuration:configuration-cli,configuration,configuration-cli,Configuration via the command-line,"The recommended way to configure Datasette is using a datasette.yaml file passed to -c/--config . You can also pass individual settings to Datasette using the -s/--setting option, which can be used multiple times: datasette mydatabase.db \ --setting settings.default_page_size 50 \ --setting settings.sql_time_limit_ms 3500 This option takes dotted-notation for the first argument and a value for the second argument. This means you can use it to set any configuration value that would be valid in a datasette.yaml file. It also works for plugin configuration, for example for datasette-cluster-map : datasette mydatabase.db \ --setting plugins.datasette-cluster-map.latitude_column xlat \ --setting plugins.datasette-cluster-map.longitude_column xlon If the value you provide is a valid JSON object or list it will be treated as nested data, allowing you to configure plugins that accept lists such as datasette-proxy-url : datasette mydatabase.db \ -s plugins.datasette-proxy-url.paths '[{""path"": ""/proxy"", ""backend"": ""http://example.com/""}]' This is equivalent to a datasette.yaml file containing the following: [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" plugins: datasette-proxy-url: paths: - path: /proxy backend: http://example.com/ """""").strip() ) ]]] [[[end]]]","[""Configuration""]","[{""href"": ""https://datasette.io/plugins/datasette-cluster-map"", ""label"": ""datasette-cluster-map""}, {""href"": ""https://datasette.io/plugins/datasette-proxy-url"", ""label"": ""datasette-proxy-url""}]" plugin_hooks:plugin-hook-extra-body-script,plugin_hooks,plugin-hook-extra-body-script,"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","[""Plugin hooks"", ""Page extras""]","[{""href"": ""https://datasette.io/plugins/datasette-cluster-map"", ""label"": ""datasette-cluster-map""}]" cli-reference:cli-help-install-help,cli-reference,cli-help-install-help,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]]]","[""CLI reference""]","[{""href"": ""https://datasette.io/plugins/datasette-cluster-map"", ""label"": ""datasette-cluster-map""}]" plugin_hooks:plugin-hook-query-actions,plugin_hooks,plugin-hook-query-actions,"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","[""Plugin hooks"", ""Action hooks""]","[{""href"": ""https://datasette.io/plugins/datasette-create-view"", ""label"": ""datasette-create-view""}]" plugin_hooks:plugin-hook-row-actions,plugin_hooks,plugin-hook-row-actions,"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","[""Plugin hooks"", ""Action hooks""]","[{""href"": ""https://datasette.io/plugins/datasette-enrichments"", ""label"": ""datasette-enrichments""}]" plugin_hooks:plugin-hook-track-event,plugin_hooks,plugin-hook-track-event,"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","[""Plugin hooks"", ""Event tracking""]","[{""href"": ""https://datasette.io/plugins/datasette-events-db"", ""label"": ""datasette-events-db""}]" plugin_hooks:plugin-hook-database-actions,plugin_hooks,plugin-hook-database-actions,"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","[""Plugin hooks"", ""Action hooks""]","[{""href"": ""https://datasette.io/plugins/datasette-graphql"", ""label"": ""datasette-graphql""}, {""href"": ""https://datasette.io/plugins/datasette-edit-schema"", ""label"": ""datasette-edit-schema""}]" plugin_hooks:plugin-hook-table-actions,plugin_hooks,plugin-hook-table-actions,"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","[""Plugin hooks"", ""Action hooks""]","[{""href"": ""https://datasette.io/plugins/datasette-graphql"", ""label"": ""datasette-graphql""}]" changelog:id17,changelog,id17,0.61.1 (2022-03-23),Fixed a bug where databases with a different route from their name (as used by the datasette-hashed-urls plugin ) returned errors when executing custom SQL queries. ( #1682 ),"[""Changelog""]","[{""href"": ""https://datasette.io/plugins/datasette-hashed-urls"", ""label"": ""datasette-hashed-urls plugin""}, {""href"": ""https://github.com/simonw/datasette/issues/1682"", ""label"": ""#1682""}]" performance:performance-hashed-urls,performance,performance-hashed-urls,datasette-hashed-urls,"If you open a database file in immutable mode using the -i option, you can be assured that the content of that database will not change for the lifetime of the Datasette server. The datasette-hashed-urls plugin implements an optimization where your database is served with part of the SHA-256 hash of the database contents baked into the URL. A database at /fixtures will instead be served at /fixtures-aa7318b , and a year-long cache expiry header will be returned with those pages. This will then be cached by both browsers and caching proxies such as Cloudflare or Fastly, providing a potentially significant performance boost. To install the plugin, run the following: datasette install datasette-hashed-urls Prior to Datasette 0.61 hashed URL mode was a core Datasette feature, enabled using the hash_urls setting. This implementation has now been removed in favor of the datasette-hashed-urls plugin. Prior to Datasette 0.28 hashed URL mode was the default behaviour for Datasette, since all database files were assumed to be immutable and unchanging. From 0.28 onwards the default has been to treat database files as mutable unless explicitly configured otherwise.","[""Performance and caching""]","[{""href"": ""https://datasette.io/plugins/datasette-hashed-urls"", ""label"": ""datasette-hashed-urls plugin""}]" plugin_hooks:plugin-hook-filters-from-request,plugin_hooks,plugin-hook-filters-from-request,"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","[""Plugin hooks""]","[{""href"": ""https://datasette.io/plugins/datasette-leaflet-freedraw"", ""label"": ""datasette-leaflet-freedraw""}]" plugin_hooks:plugin-hook-permission-allowed,plugin_hooks,plugin-hook-permission-allowed,"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","[""Plugin hooks""]","[{""href"": ""https://datasette.io/plugins/datasette-permissions-sql"", ""label"": ""datasette-permissions-sql""}]" plugin_hooks:plugin-hook-render-cell,plugin_hooks,plugin-hook-render-cell,"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( '<a href=""{href}"">{label}</a>'.format( href=markupsafe.escape(data[""href""]), label=markupsafe.escape(data[""label""] or """") or "" "", ) ) Examples: datasette-render-binary , datasette-render-markdown , datasette-json-html","[""Plugin hooks""]","[{""href"": ""https://datasette.io/plugins/datasette-render-binary"", ""label"": ""datasette-render-binary""}, {""href"": ""https://datasette.io/plugins/datasette-render-markdown"", ""label"": ""datasette-render-markdown""}, {""href"": ""https://datasette.io/plugins/datasette-json-html"", ""label"": ""datasette-json-html""}]" plugin_hooks:plugin-hook-startup,plugin_hooks,plugin-hook-startup,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","[""Plugin hooks""]","[{""href"": ""https://datasette.io/plugins/datasette-saved-queries"", ""label"": ""datasette-saved-queries""}, {""href"": ""https://datasette.io/plugins/datasette-init"", ""label"": ""datasette-init""}]" plugin_hooks:plugin-hook-canned-queries,plugin_hooks,plugin-hook-canned-queries,"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","[""Plugin hooks""]","[{""href"": ""https://datasette.io/plugins/datasette-saved-queries"", ""label"": ""datasette-saved-queries""}]" plugin_hooks:plugin-hook-menu-links,plugin_hooks,plugin-hook-menu-links,"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","[""Plugin hooks""]","[{""href"": ""https://datasette.io/plugins/datasette-search-all"", ""label"": ""datasette-search-all""}, {""href"": ""https://datasette.io/plugins/datasette-graphql"", ""label"": ""datasette-graphql""}]" getting_started:getting-started-tutorial,getting_started,getting-started-tutorial,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.","[""Getting started""]","[{""href"": ""https://datasette.io/tutorials"", ""label"": ""tutorials""}, {""href"": ""https://datasette.io/tutorials/explore"", ""label"": ""Exploring a database with Datasette""}, {""href"": ""https://datasette.io/tutorials/learn-sql"", ""label"": ""Learn SQL with Datasette""}, {""href"": ""https://datasette.io/tutorials/clean-data"", ""label"": ""Cleaning data with sqlite-utils and Datasette""}, {""href"": ""https://sqlite-utils.datasette.io/"", ""label"": ""sqlite-utils""}]" changelog:id12,changelog,id12,Documentation,"New tutorial: Cleaning data with sqlite-utils and Datasette . Screenshots in the documentation are now maintained using shot-scraper , as described in Automating screenshots for the Datasette documentation using shot-scraper . ( #1844 ) More detailed command descriptions on the CLI reference page. ( #1787 ) New documentation on Running Datasette using OpenRC - thanks, Adam Simpson. ( #1825 )","[""Changelog"", ""0.63 (2022-10-27)""]","[{""href"": ""https://datasette.io/tutorials/clean-data"", ""label"": ""Cleaning data with sqlite-utils and Datasette""}, {""href"": ""https://shot-scraper.datasette.io/"", ""label"": ""shot-scraper""}, {""href"": ""https://simonwillison.net/2022/Oct/14/automating-screenshots/"", ""label"": ""Automating screenshots for the Datasette documentation using shot-scraper""}, {""href"": ""https://github.com/simonw/datasette/issues/1844"", ""label"": ""#1844""}, {""href"": ""https://github.com/simonw/datasette/issues/1787"", ""label"": ""#1787""}, {""href"": ""https://github.com/simonw/datasette/pull/1825"", ""label"": ""#1825""}]" internals:internals-tilde-encoding,internals,internals-tilde-encoding,Tilde encoding,"Datasette uses a custom encoding scheme in some places, called tilde encoding . This is primarily used for table names and row primary keys, to avoid any confusion between / characters in those values and the Datasette URLs that reference them. Tilde encoding uses the same algorithm as URL percent-encoding , but with the ~ tilde character used in place of % . Any character other than ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz0123456789_- will be replaced by the numeric equivalent preceded by a tilde. For example: / becomes ~2F . becomes ~2E % becomes ~25 ~ becomes ~7E Space becomes + polls/2022.primary becomes polls~2F2022~2Eprimary Note that the space character is a special case: it will be replaced with a + symbol. datasette.utils. tilde_encode s : str str Returns tilde-encoded string - for example /foo/bar -> ~2Ffoo~2Fbar datasette.utils. tilde_decode s : str str Decodes a tilde-encoded string, so ~2Ffoo~2Fbar -> /foo/bar","[""Internals for plugins"", ""The datasette.utils module""]","[{""href"": ""https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding"", ""label"": ""URL percent-encoding""}]" plugin_hooks:plugin-hook-extra-js-urls,plugin_hooks,plugin-hook-extra-js-urls,"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","[""Plugin hooks"", ""Page extras""]","[{""href"": ""https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules"", ""label"": ""JavaScript modules""}, {""href"": ""https://datasette.io/plugins/datasette-cluster-map"", ""label"": ""datasette-cluster-map""}, {""href"": ""https://datasette.io/plugins/datasette-vega"", ""label"": ""datasette-vega""}]" changelog:javascript-modules,changelog,javascript-modules,JavaScript modules,"JavaScript modules were introduced in ECMAScript 2015 and provide native browser support for the import and export keywords. To use modules, JavaScript needs to be included in <script> tags with a type=""module"" attribute. Datasette now has the ability to output <script type=""module""> in places where you may wish to take advantage of modules. The extra_js_urls option described in Custom CSS and JavaScript can now be used with modules, and module support is also available for the extra_body_script() plugin hook. ( #1186 , #1187 ) datasette-leaflet-freedraw is the first example of a Datasette plugin that takes advantage of the new support for JavaScript modules. See Drawing shapes on a map to query a SpatiaLite database for more on this plugin.","[""Changelog"", ""0.54 (2021-01-25)""]","[{""href"": ""https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules"", ""label"": ""JavaScript modules""}, {""href"": ""https://github.com/simonw/datasette/issues/1186"", ""label"": ""#1186""}, {""href"": ""https://github.com/simonw/datasette/issues/1187"", ""label"": ""#1187""}, {""href"": ""https://datasette.io/plugins/datasette-leaflet-freedraw"", ""label"": ""datasette-leaflet-freedraw""}, {""href"": ""https://simonwillison.net/2021/Jan/24/drawing-shapes-spatialite/"", ""label"": ""Drawing shapes on a map to query a SpatiaLite database""}]" changelog:id52,changelog,id52,0.48 (2020-08-16),"Datasette documentation now lives at docs.datasette.io . db.is_mutable property is now documented and tested, see Database introspection . The extra_template_vars , extra_css_urls , extra_js_urls and extra_body_script plugin hooks now all accept the same arguments. See extra_template_vars(template, database, table, columns, view_name, request, datasette) for details. ( #939 ) Those hooks now accept a new columns argument detailing the table columns that will be rendered on that page. ( #938 ) Fixed bug where plugins calling db.execute_write_fn() could hang Datasette if the connection failed. ( #935 ) Fixed bug with the ?_nl=on output option and binary data. ( #914 )","[""Changelog""]","[{""href"": ""https://docs.datasette.io/"", ""label"": ""docs.datasette.io""}, {""href"": ""https://github.com/simonw/datasette/issues/939"", ""label"": ""#939""}, {""href"": ""https://github.com/simonw/datasette/issues/938"", ""label"": ""#938""}, {""href"": ""https://github.com/simonw/datasette/issues/935"", ""label"": ""#935""}, {""href"": ""https://github.com/simonw/datasette/issues/914"", ""label"": ""#914""}]" changelog:id174,changelog,id174,0.14 (2017-12-09),"The theme of this release is customization: Datasette now allows every aspect of its presentation to be customized either using additional CSS or by providing entirely new templates. Datasette's metadata.json format has also been expanded, to allow per-database and per-table metadata. A new datasette skeleton command can be used to generate a skeleton JSON file ready to be filled in with per-database and per-table details. The metadata.json file can also be used to define canned queries , as a more powerful alternative to SQL views. extra_css_urls / extra_js_urls in metadata A mechanism in the metadata.json format for adding custom CSS and JS urls. Create a metadata.json file that looks like this: { ""extra_css_urls"": [ ""https://simonwillison.net/static/css/all.bf8cd891642c.css"" ], ""extra_js_urls"": [ ""https://code.jquery.com/jquery-3.2.1.slim.min.js"" ] } Then start datasette like this: datasette mydb.db --metadata=metadata.json The CSS and JavaScript files will be linked in the <head> of every page. You can also specify a SRI (subresource integrity hash) for these assets: { ""extra_css_urls"": [ { ""url"": ""https://simonwillison.net/static/css/all.bf8cd891642c.css"", ""sri"": ""sha384-9qIZekWUyjCyDIf2YK1FRoKiPJq4PHt6tp/ulnuuyRBvazd0hG7pWbE99zvwSznI"" } ], ""extra_js_urls"": [ { ""url"": ""https://code.jquery.com/jquery-3.2.1.slim.min.js"", ""sri"": ""sha256-k2WSCIexGzOj3Euiig+TlR8gA0EmPjuc79OEeY5L45g="" } ] } Modern browsers will only execute the stylesheet or JavaScript if the SRI hash matches the content served. You can generate hashes using https://www.srihash.org/ Auto-link column values that look like URLs ( #153 ) CSS styling hooks as classes on the body ( #153 ) Every template now gets CSS classes in the body designed to support custom styling. The index template (the top level page at / ) gets this: <body class=""index""> The database template ( /dbname/ ) gets this: <body class=""db db-dbname""> The table template ( /dbname/tablename ) gets: <body class=""table db-dbname table-tablename""> The row template ( /dbname/tablename/rowid ) gets: <body class=""row db-dbname table-tablename""> 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 (extracted from the unit tests): ""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"" datasette --template-dir=mytemplates/ argument You can now pass an additional argument specifying a directory to look for custom templates in. Datasette will fall back on the default templates if a template is not found in that directory. Ability to over-ride templates for individual tables/databases. It is now possible to over-ride templates on a per-database / per-row or per- table basis. When you access e.g. /mydatabase/mytable Datasette will look for the following: - table-mydatabase-mytable.html - table.html If you provided a --template-dir argument to datasette serve it will look in that directory first. The lookup rules are as follows: Index page (/): index.html Database page (/mydatabase): database-mydatabase.html database.html Table page (/mydatabase/mytable): table-mydatabase-mytable.html table.html Row page (/mydatabase/mytable/id): row-mydatabase-mytable.html row.html If a table name has spaces or other unexpected characters in it, the template filename will follow the same rules as our custom <body> CSS classes - for example, a table called ""Food Trucks"" will attempt to load the following templates: table-mydatabase-Food-Trucks-399138.html table.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 %} <h1>EXTRA HTML AT THE TOP OF THE CONTENT BLOCK</h1> <p>This line renders the original block:</p> {{ super() }} {% endblock %} --static option for datasette serve ( #160 ) You can now tell Datasette to serve static files from a specific location at a specific mountpoint. For example: datasette serve mydb.db --static extra-css:/tmp/static/css Now if you visit this URL: http://localhost:8001/extra-css/blah.css The following file will be served: /tmp/static/css/blah.css Canned query support. Named canned queries can now be defined in metadata.json like this: { ""databases"": { ""timezones"": { ""queries"": { ""timezone_for_point"": ""select tzid from timezones ..."" } } } } These will be shown in a new ""Queries"" section beneath ""Views"" on the database page. New datasette skeleton command for generating metadata.json ( #164 ) metadata.json support for per-table/per-database metadata ( #165 ) Also added support for descriptions and HTML descriptions. Here's an example metadata.json file illustrating custom per-database and per- table metadata: { ""title"": ""Overall datasette title"", ""description_html"": ""This is a <em>description with HTML</em>."", ""databases"": { ""db1"": { ""title"": ""First database"", ""description"": ""This is a string description & has no HTML"", ""license_url"": ""http://example.com/"", ""license"": ""The example license"", ""queries"": { ""canned_query"": ""select * from table1 limit 3;"" }, ""tables"": { ""table1"": { ""title"": ""Custom title for table1"", ""description"": ""Tables can have descriptions too"", ""source"": ""This has a custom source"", ""source_url"": ""http://example.com/"" } } } } } Renamed datasette build command to datasette inspect ( #130 ) Upgrade to Sanic 0.7.0 ( #168 ) https://github.com/channelcat/sanic/releases/tag/0.7.0 Package and publish commands now accept --static and --template-dir Example usage: datasette package --static css:extra-css/ --static js:extra-js/ \ sf-trees.db --template-dir templates/ --tag sf-trees --branch master This creates a local Docker image that includes copies of the templates/, extra-css/ and extra-js/ directories. You can then run it like this: docker run -p 8001:8001 sf-trees For publishing to Zeit now: datasette publish now --static css:extra-css/ --static js:extra-js/ \ sf-trees.db --template-dir templates/ --name sf-trees --branch master HTML comment showing which templates were considered for a page ( #171 )","[""Changelog""]","[{""href"": ""https://docs.datasette.io/en/stable/custom_templates.html"", ""label"": ""to be customized""}, {""href"": ""https://docs.datasette.io/en/stable/metadata.html"", ""label"": ""metadata.json format""}, {""href"": ""https://docs.datasette.io/en/stable/sql_queries.html#canned-queries"", ""label"": ""canned queries""}, {""href"": ""https://www.srihash.org/"", ""label"": ""https://www.srihash.org/""}, {""href"": ""https://github.com/simonw/datasette/issues/153"", ""label"": ""#153""}, {""href"": ""https://github.com/simonw/datasette/issues/153"", ""label"": ""#153""}, {""href"": ""https://github.com/simonw/datasette/issues/160"", ""label"": ""#160""}, {""href"": ""https://github.com/simonw/datasette/issues/164"", ""label"": ""#164""}, {""href"": ""https://github.com/simonw/datasette/issues/165"", ""label"": ""#165""}, {""href"": ""https://github.com/simonw/datasette/issues/130"", ""label"": ""#130""}, {""href"": ""https://github.com/simonw/datasette/issues/168"", ""label"": ""#168""}, {""href"": ""https://github.com/channelcat/sanic/releases/tag/0.7.0"", ""label"": ""https://github.com/channelcat/sanic/releases/tag/0.7.0""}, {""href"": ""https://github.com/simonw/datasette/issues/171"", ""label"": ""#171""}]" changelog:id152,changelog,id152,0.18 (2018-04-14),"This release introduces support for units , contributed by Russ Garrett ( #203 ). You can now optionally specify the units for specific columns using metadata.json . Once specified, units will be displayed in the HTML view of your table. They also become available for use in filters - if a column is configured with a unit of distance, you can request all rows where that column is less than 50 meters or more than 20 feet for example. Link foreign keys which don't have labels. [Russ Garrett] This renders unlabeled FKs as simple links. Also includes bonus fixes for two minor issues: In foreign key link hrefs the primary key was escaped using HTML escaping rather than URL escaping. This broke some non-integer PKs. Print tracebacks to console when handling 500 errors. Fix SQLite error when loading rows with no incoming FKs. [Russ Garrett] This fixes an error caused by an invalid query when loading incoming FKs. The error was ignored due to async but it still got printed to the console. Allow custom units to be registered with Pint. [Russ Garrett] Support units in filters. [Russ Garrett] Tidy up units support. [Russ Garrett] Add units to exported JSON Units key in metadata skeleton Docs Initial units support. [Russ Garrett] Add support for specifying units for a column in metadata.json and rendering them on display using pint","[""Changelog""]","[{""href"": ""https://docs.datasette.io/en/stable/metadata.html#specifying-units-for-a-column"", ""label"": ""support for units""}, {""href"": ""https://github.com/simonw/datasette/issues/203"", ""label"": ""#203""}, {""href"": ""https://pint.readthedocs.io/en/latest/"", ""label"": ""pint""}]" changelog:id145,changelog,id145,0.19 (2018-04-16),"This is the first preview of the new Datasette plugins mechanism. Only two plugin hooks are available so far - for custom SQL functions and custom template filters. There's plenty more to come - read the documentation and get involved in the tracking ticket if you have feedback on the direction so far. Fix for _sort_desc=sortable_with_nulls test, refs #216 Fixed #216 - paginate correctly when sorting by nullable column Initial documentation for plugins, closes #213 https://docs.datasette.io/en/stable/plugins.html New --plugins-dir=plugins/ option ( #212 ) New option causing Datasette to load and evaluate all of the Python files in the specified directory and register any plugins that are defined in those files. This new option is available for the following commands: datasette serve mydb.db --plugins-dir=plugins/ datasette publish now/heroku mydb.db --plugins-dir=plugins/ datasette package mydb.db --plugins-dir=plugins/ Start of the plugin system, based on pluggy ( #210 ) Uses https://pluggy.readthedocs.io/ originally created for the py.test project We're starting with two plugin hooks: prepare_connection(conn) This is called when a new SQLite connection is created. It can be used to register custom SQL functions. prepare_jinja2_environment(env) This is called with the Jinja2 environment. It can be used to register custom template tags and filters. An example plugin which uses these two hooks can be found at https://github.com/simonw/datasette-plugin-demos or installed using pip install datasette-plugin-demos Refs #14 Return HTTP 405 on InvalidUsage rather than 500. [Russ Garrett] This also stops it filling up the logs. This happens for HEAD requests at the moment - which perhaps should be handled better, but that's a different issue.","[""Changelog""]","[{""href"": ""https://docs.datasette.io/en/stable/plugins.html"", ""label"": ""the documentation""}, {""href"": ""https://github.com/simonw/datasette/issues/14"", ""label"": ""the tracking ticket""}, {""href"": ""https://github.com/simonw/datasette/issues/216"", ""label"": ""#216""}, {""href"": ""https://github.com/simonw/datasette/issues/216"", ""label"": ""#216""}, {""href"": ""https://github.com/simonw/datasette/issues/213"", ""label"": ""#213""}, {""href"": ""https://docs.datasette.io/en/stable/plugins.html"", ""label"": ""https://docs.datasette.io/en/stable/plugins.html""}, {""href"": ""https://github.com/simonw/datasette/issues/212"", ""label"": ""#212""}, {""href"": ""https://github.com/simonw/datasette/issues/14"", ""label"": ""#210""}, {""href"": ""https://pluggy.readthedocs.io/"", ""label"": ""https://pluggy.readthedocs.io/""}, {""href"": ""https://github.com/simonw/datasette-plugin-demos"", ""label"": ""https://github.com/simonw/datasette-plugin-demos""}, {""href"": ""https://github.com/simonw/datasette/issues/14"", ""label"": ""#14""}]" contributing:contributing-running-tests,contributing,contributing-running-tests,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""","[""Contributing""]","[{""href"": ""https://docs.pytest.org/"", ""label"": ""pytest""}, {""href"": ""https://pypi.org/project/pytest-xdist/"", ""label"": ""pytest-xdist""}]" testing_plugins:id1,testing_plugins,id1,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",[],"[{""href"": ""https://docs.pytest.org/"", ""label"": ""pytest""}, {""href"": ""https://www.python-httpx.org/"", ""label"": ""HTTPX""}, {""href"": ""https://pypi.org/project/pytest-asyncio/"", ""label"": ""pytest-asyncio""}]" testing_plugins:testing-plugins-fixtures,testing_plugins,testing-plugins-fixtures,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</h1>"" 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 ...","[""Testing plugins""]","[{""href"": ""https://docs.pytest.org/en/stable/fixture.html"", ""label"": ""Pytest fixtures""}, {""href"": ""https://sqlite-utils.datasette.io/en/stable/python-api.html"", ""label"": ""sqlite-utils library""}]" contributing:devenvironment,contributing,devenvironment,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"".","[""Contributing""]","[{""href"": ""https://docs.python-guide.org/starting/install3/osx/"", ""label"": ""is using homebrew""}, {""href"": ""https://github.com/simonw/datasette/fork"", ""label"": ""create a fork of datasette""}]" plugin_hooks:plugin-hook-prepare-connection,plugin_hooks,plugin-hook-prepare-connection,"prepare_connection(conn, database, datasette)","conn - sqlite3 connection object The connection that is being opened database - string The name of the database datasette - 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","[""Plugin hooks""]","[{""href"": ""https://docs.python.org/2/library/sqlite3.html#sqlite3.Connection.create_function"", ""label"": ""register custom SQL functions""}, {""href"": ""https://datasette.io/plugins/datasette-jellyfish"", ""label"": ""datasette-jellyfish""}, {""href"": ""https://datasette.io/plugins/datasette-jq"", ""label"": ""datasette-jq""}, {""href"": ""https://datasette.io/plugins/datasette-haversine"", ""label"": ""datasette-haversine""}, {""href"": ""https://datasette.io/plugins/datasette-rure"", ""label"": ""datasette-rure""}]" internals:database-results,internals,database-results,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.","[""Internals for plugins"", ""Database class""]","[{""href"": ""https://docs.python.org/3/library/sqlite3.html#row-objects"", ""label"": ""Row objects""}]" internals:database-execute-write-many,internals,database-execute-write-many,"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.","[""Internals for plugins"", ""Database class""]","[{""href"": ""https://docs.python.org/3/library/sqlite3.html#sqlite3.Cursor.executemany"", ""label"": ""conn.executemany()""}]" internals:database-execute-write-script,internals,database-execute-write-script,"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.","[""Internals for plugins"", ""Database class""]","[{""href"": ""https://docs.python.org/3/library/sqlite3.html#sqlite3.Cursor.executescript"", ""label"": ""conn.executescript()""}]" ecosystem:dogsheep,ecosystem,dogsheep,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.,"[""The Datasette Ecosystem""]","[{""href"": ""https://dogsheep.github.io/"", ""label"": ""Dogsheep""}, {""href"": ""https://datasette.io/tools/github-to-sqlite"", ""label"": ""github-to-sqlite""}, {""href"": ""https://datasette.io/tools/twitter-to-sqlite"", ""label"": ""twitter-to-sqlite""}, {""href"": ""https://simonwillison.net/2020/Nov/14/personal-data-warehouses/"", ""label"": ""Personal Data Warehouses: Reclaiming Your Data""}]" spatialite:importing-shapefiles-into-spatialite,spatialite,importing-shapefiles-into-spatialite,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;","[""SpatiaLite""]","[{""href"": ""https://en.wikipedia.org/wiki/Shapefile"", ""label"": ""shapefile format""}, {""href"": ""http://gaia.geosci.unc.edu/rivers/"", ""label"": ""Global River Database""}, {""href"": ""https://www.gaia-gis.it/gaia-sins/BLOB-Geometry.html"", ""label"": ""a custom format based on WKB""}, {""href"": ""https://geojson.io/"", ""label"": ""geojson.io""}]" full_text_search:full-text-search-table-view-api,full_text_search,full-text-search-table-view-api,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","[""Full-text search""]","[{""href"": ""https://fara.datasettes.com/fara/FARA_All_ShortForms?_search=manafort"", ""label"": ""fara.datasettes.com/fara/FARA_All_ShortForms?_search=manafort""}]" full_text_search:full-text-search-custom-sql,full_text_search,full-text-search-custom-sql,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","[""Full-text search""]","[{""href"": ""https://fara.datasettes.com/fara/FARA_All_ShortForms?_search=manafort"", ""label"": ""manafort is the US FARA database""}, {""href"": ""https://fara.datasettes.com/fara?sql=select%0D%0A++rowid%2C%0D%0A++Short_Form_Termination_Date%2C%0D%0A++Short_Form_Date%2C%0D%0A++Short_Form_Last_Name%2C%0D%0A++Short_Form_First_Name%2C%0D%0A++Registration_Number%2C%0D%0A++Registration_Date%2C%0D%0A++Registrant_Name%2C%0D%0A++Address_1%2C%0D%0A++Address_2%2C%0D%0A++City%2C%0D%0A++State%2C%0D%0A++Zip%0D%0Afrom%0D%0A++FARA_All_ShortForms%0D%0Awhere%0D%0A++rowid+in+%28%0D%0A++++select%0D%0A++++++rowid%0D%0A++++from%0D%0A++++++FARA_All_ShortForms_fts%0D%0A++++where%0D%0A++++++FARA_All_ShortForms_fts+match+escape_fts%28%3Asearch%29%0D%0A++%29%0D%0Aorder+by%0D%0A++rowid%0D%0Alimit%0D%0A++101&search=manafort"", ""label"": ""View and edit SQL""}]" pages:indexview,pages,indexview,Top-level index,"The root page of any Datasette installation is an index page that lists all of the currently attached databases. Some examples: fivethirtyeight.datasettes.com global-power-plants.datasettes.com register-of-members-interests.datasettes.com Add /.json to the end of the URL for the JSON version of the underlying data: fivethirtyeight.datasettes.com/.json global-power-plants.datasettes.com/.json register-of-members-interests.datasettes.com/.json","[""Pages and API endpoints""]","[{""href"": ""https://fivethirtyeight.datasettes.com/"", ""label"": ""fivethirtyeight.datasettes.com""}, {""href"": ""https://global-power-plants.datasettes.com/"", ""label"": ""global-power-plants.datasettes.com""}, {""href"": ""https://register-of-members-interests.datasettes.com/"", ""label"": ""register-of-members-interests.datasettes.com""}, {""href"": ""https://fivethirtyeight.datasettes.com/.json"", ""label"": ""fivethirtyeight.datasettes.com/.json""}, {""href"": ""https://global-power-plants.datasettes.com/.json"", ""label"": ""global-power-plants.datasettes.com/.json""}, {""href"": ""https://register-of-members-interests.datasettes.com/.json"", ""label"": ""register-of-members-interests.datasettes.com/.json""}]" introspection:jsondataview-metadata,introspection,jsondataview-metadata,/-/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"": { } }","[""Introspection""]","[{""href"": ""https://fivethirtyeight.datasettes.com/-/metadata"", ""label"": ""Metadata example""}]" plugins:plugins-installed,plugins,plugins-installed,Seeing what plugins are installed,"You can see a list of installed plugins by navigating to the /-/plugins page of your Datasette instance - for example: https://fivethirtyeight.datasettes.com/-/plugins You can also use the datasette plugins command: datasette plugins Which outputs: [ { ""name"": ""datasette_json_html"", ""static"": false, ""templates"": false, ""version"": ""0.4.0"" } ] [[[cog from datasette import cli from click.testing import CliRunner import textwrap, json cog.out(""\n"") result = CliRunner().invoke(cli.cli, [""plugins"", ""--all""]) # cog.out() with text containing newlines was unindenting for some reason cog.outl(""If you run ``datasette plugins --all`` it will include default plugins that ship as part of Datasette:\n"") cog.outl("".. code-block:: json\n"") plugins = [p for p in json.loads(result.output) if p[""name""].startswith(""datasette."")] indented = textwrap.indent(json.dumps(plugins, indent=4), "" "") for line in indented.split(""\n""): cog.outl(line) cog.out(""\n\n"") ]]] If you run datasette plugins --all it will include default plugins that ship as part of Datasette: [ { ""name"": ""datasette.actor_auth_cookie"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""actor_from_request"" ] }, { ""name"": ""datasette.blob_renderer"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""register_output_renderer"" ] }, { ""name"": ""datasette.default_magic_parameters"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""register_magic_parameters"" ] }, { ""name"": ""datasette.default_menu_links"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""menu_links"" ] }, { ""name"": ""datasette.default_permissions"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""actor_from_request"", ""permission_allowed"", ""register_permissions"", ""skip_csrf"" ] }, { ""name"": ""datasette.events"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""register_events"" ] }, { ""name"": ""datasette.facets"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""register_facet_classes"" ] }, { ""name"": ""datasette.filters"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""filters_from_request"" ] }, { ""name"": ""datasette.forbidden"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""forbidden"" ] }, { ""name"": ""datasette.handle_exception"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""handle_exception"" ] }, { ""name"": ""datasette.publish.cloudrun"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""publish_subcommand"" ] }, { ""name"": ""datasette.publish.heroku"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""publish_subcommand"" ] }, { ""name"": ""datasette.sql_functions"", ""static"": false, ""templates"": false, ""version"": null, ""hooks"": [ ""prepare_connection"" ] } ] [[[end]]] You can add the --plugins-dir= option to include any plugins found in that directory. Add --requirements to output a list of installed plugins that can then be installed in another Datasette instance using datasette install -r requirements.txt : datasette plugins --requirements The output will look something like this: datasette-codespaces==0.1.1 datasette-graphql==2.2 datasette-json-html==1.0.1 datasette-pretty-json==0.2.2 datasette-x-forwarded-host==0.1 To write that to a requirements.txt file, run this: datasette plugins --requirements > requirements.txt","[""Plugins""]","[{""href"": ""https://fivethirtyeight.datasettes.com/-/plugins"", ""label"": ""https://fivethirtyeight.datasettes.com/-/plugins""}]" introspection:jsondataview-settings,introspection,jsondataview-settings,/-/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 }","[""Introspection""]","[{""href"": ""https://fivethirtyeight.datasettes.com/-/settings"", ""label"": ""Settings example""}]" pages:databaseview,pages,databaseview,Database,"Each database has a page listing the tables, views and canned queries available for that database. If the execute-sql permission is enabled (it's on by default) there will also be an interface for executing arbitrary SQL select queries against the data. Examples: fivethirtyeight.datasettes.com/fivethirtyeight global-power-plants.datasettes.com/global-power-plants The JSON version of this page provides programmatic access to the underlying data: fivethirtyeight.datasettes.com/fivethirtyeight.json global-power-plants.datasettes.com/global-power-plants.json","[""Pages and API endpoints""]","[{""href"": ""https://fivethirtyeight.datasettes.com/fivethirtyeight"", ""label"": ""fivethirtyeight.datasettes.com/fivethirtyeight""}, {""href"": ""https://global-power-plants.datasettes.com/global-power-plants"", ""label"": ""global-power-plants.datasettes.com/global-power-plants""}, {""href"": ""https://fivethirtyeight.datasettes.com/fivethirtyeight.json"", ""label"": ""fivethirtyeight.datasettes.com/fivethirtyeight.json""}, {""href"": ""https://global-power-plants.datasettes.com/global-power-plants.json"", ""label"": ""global-power-plants.datasettes.com/global-power-plants.json""}]" json_api:json-api-special,json_api,json-api-special,Special JSON arguments,"Every Datasette endpoint that can return JSON also accepts the following query string arguments: ?_shape=SHAPE The shape of the JSON to return, documented above. ?_nl=on When used with ?_shape=array produces newline-delimited JSON objects. ?_json=COLUMN1&_json=COLUMN2 If any of your SQLite columns contain JSON values, you can use one or more _json= parameters to request that those columns be returned as regular JSON. Without this argument those columns will be returned as JSON objects that have been double-encoded into a JSON string value. Compare this query without the argument to this query using the argument ?_json_infinity=on If your data contains infinity or -infinity values, Datasette will replace them with None when returning them as JSON. If you pass _json_infinity=1 Datasette will instead return them as Infinity or -Infinity which is invalid JSON but can be processed by some custom JSON parsers. ?_timelimit=MS Sets a custom time limit for the query in ms. You can use this for optimistic queries where you would like Datasette to give up if the query takes too long, for example if you want to implement autocomplete search but only if it can be executed in less than 10ms. ?_ttl=SECONDS For how many seconds should this response be cached by HTTP proxies? Use ?_ttl=0 to disable HTTP caching entirely for this request. ?_trace=1 Turns on tracing for this page: SQL queries executed during the request will be gathered and included in the response, either in a new ""_traces"" key for JSON responses or at the bottom of the page if the response is in HTML. The structure of the data returned here should be considered highly unstable and very likely to change. Only available if the trace_debug setting is enabled.","[""JSON API""]","[{""href"": ""https://fivethirtyeight.datasettes.com/fivethirtyeight.json?sql=select+%27{%22this+is%22%3A+%22a+json+object%22}%27+as+d&_shape=array"", ""label"": ""this query without the argument""}, {""href"": ""https://fivethirtyeight.datasettes.com/fivethirtyeight.json?sql=select+%27{%22this+is%22%3A+%22a+json+object%22}%27+as+d&_shape=array&_json=d"", ""label"": ""this query using the argument""}]" changelog:csv-export,changelog,csv-export,CSV export,"Any Datasette table, view or custom SQL query can now be exported as CSV. Check out the CSV export documentation for more details, or try the feature out on https://fivethirtyeight.datasettes.com/fivethirtyeight/bechdel%2Fmovies If your table has more than max_returned_rows (default 1,000) Datasette provides the option to stream all rows . This option takes advantage of async Python and Datasette's efficient pagination to iterate through the entire matching result set and stream it back as a downloadable CSV file.","[""Changelog"", ""0.23 (2018-06-18)""]","[{""href"": ""https://fivethirtyeight.datasettes.com/fivethirtyeight/bechdel%2Fmovies"", ""label"": ""https://fivethirtyeight.datasettes.com/fivethirtyeight/bechdel%2Fmovies""}]" changelog:control-http-caching-with-ttl,changelog,control-http-caching-with-ttl,Control HTTP caching with ?_ttl=,"You can now customize the HTTP max-age header that is sent on a per-URL basis, using the new ?_ttl= query string parameter. You can set this to any value in seconds, or you can set it to 0 to disable HTTP caching entirely. Consider for example this query which returns a randomly selected member of the Avengers: select * from [avengers/avengers] order by random() limit 1 If you hit the following page repeatedly you will get the same result, due to HTTP caching: /fivethirtyeight?sql=select+*+from+%5Bavengers%2Favengers%5D+order+by+random%28%29+limit+1 By adding ?_ttl=0 to the zero you can ensure the page will not be cached and get back a different super hero every time: /fivethirtyeight?sql=select+*+from+%5Bavengers%2Favengers%5D+order+by+random%28%29+limit+1&_ttl=0","[""Changelog"", ""0.23 (2018-06-18)""]","[{""href"": ""https://fivethirtyeight.datasettes.com/fivethirtyeight?sql=select+*+from+%5Bavengers%2Favengers%5D+order+by+random%28%29+limit+1"", ""label"": ""/fivethirtyeight?sql=select+*+from+%5Bavengers%2Favengers%5D+order+by+random%28%29+limit+1""}, {""href"": ""https://fivethirtyeight.datasettes.com/fivethirtyeight?sql=select+*+from+%5Bavengers%2Favengers%5D+order+by+random%28%29+limit+1&_ttl=0"", ""label"": ""/fivethirtyeight?sql=select+*+from+%5Bavengers%2Favengers%5D+order+by+random%28%29+limit+1&_ttl=0""}]" publish:publish-fly,publish,publish-fly,Publishing to Fly,"Fly is a competitively priced Docker-compatible hosting platform that supports running applications in globally distributed data centers close to your end users. You can deploy Datasette instances to Fly using the datasette-publish-fly plugin. pip install datasette-publish-fly datasette publish fly mydatabase.db --app=""my-app"" Consult the datasette-publish-fly README for more details.","[""Publishing data"", ""datasette publish""]","[{""href"": ""https://fly.io/"", ""label"": ""Fly""}, {""href"": ""https://fly.io/docs/pricing/"", ""label"": ""competitively priced""}, {""href"": ""https://github.com/simonw/datasette-publish-fly"", ""label"": ""datasette-publish-fly""}, {""href"": ""https://github.com/simonw/datasette-publish-fly/blob/main/README.md"", ""label"": ""datasette-publish-fly README""}]" changelog:id39,changelog,id39,0.52.4 (2020-12-05),"Show pysqlite3 version on /-/versions , if installed. ( #1125 ) Errors output by Datasette (e.g. for invalid SQL queries) now go to stderr , not stdout . ( #1131 ) Fix for a startup error on windows caused by unnecessary from os import EX_CANTCREAT - thanks, Abdussamet KoΓ§ak. ( #1094 )","[""Changelog""]","[{""href"": ""https://github.com/coleifer/pysqlite3"", ""label"": ""pysqlite3""}, {""href"": ""https://github.com/simonw/datasette/issues/1125"", ""label"": ""#1125""}, {""href"": ""https://github.com/simonw/datasette/issues/1131"", ""label"": ""#1131""}, {""href"": ""https://github.com/simonw/datasette/issues/1094"", ""label"": ""#1094""}]" plugin_hooks:plugin-hook-actors-from-ids,plugin_hooks,plugin-hook-actors-from-ids,"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","[""Plugin hooks""]","[{""href"": ""https://github.com/datasette/datasette-remote-actors"", ""label"": ""datasette-remote-actors""}]" internals:database-execute-isolated-fn,internals,database-execute-isolated-fn,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.","[""Internals for plugins"", ""Database class""]","[{""href"": ""https://github.com/datasette/datasette-sqlite-authorizer"", ""label"": ""datasette-sqlite-authorizer""}]" changelog:javascript-plugins,changelog,javascript-plugins,JavaScript plugins,"Datasette now includes a JavaScript plugins mechanism , allowing JavaScript to customize Datasette in a way that can collaborate with other plugins. This provides two initial hooks, with more to come in the future: makeAboveTablePanelConfigs() can add additional panels to the top of the table page. makeColumnActions() can add additional actions to the column menu. Thanks Cameron Yick for contributing this feature. ( #2052 )","[""Changelog"", ""1.0a8 (2024-02-07)""]","[{""href"": ""https://github.com/hydrosquall"", ""label"": ""Cameron Yick""}, {""href"": ""https://github.com/simonw/datasette/pull/2052"", ""label"": ""#2052""}]" contributing:contributing-documentation-cog,contributing,contributing-documentation-cog,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","[""Contributing"", ""Editing and building the documentation""]","[{""href"": ""https://github.com/nedbat/cog"", ""label"": ""Cog""}]" changelog:code-formatting-with-black-and-prettier,changelog,code-formatting-with-black-and-prettier,Code formatting with Black and Prettier,"Datasette adopted Black for opinionated Python code formatting in June 2019. Datasette now also embraces Prettier for JavaScript formatting, which like Black is enforced by tests in continuous integration. Instructions for using these two tools can be found in the new section on Code formatting in the contributors documentation. ( #1167 )","[""Changelog"", ""0.54 (2021-01-25)""]","[{""href"": ""https://github.com/psf/black"", ""label"": ""Black""}, {""href"": ""https://prettier.io/"", ""label"": ""Prettier""}, {""href"": ""https://github.com/simonw/datasette/issues/1167"", ""label"": ""#1167""}]" contributing:contributing-formatting,contributing,contributing-formatting,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.","[""Contributing""]","[{""href"": ""https://github.com/psf/black"", ""label"": ""Black""}, {""href"": ""https://prettier.io/"", ""label"": ""Prettier""}]" changelog:v0-28-medium-changes,changelog,v0-28-medium-changes,Medium changes,"Datasette now conforms to the Black coding style ( #449 ) - and has a unit test to enforce this in the future New Special table arguments : ?columnname__in=value1,value2,value3 filter for executing SQL IN queries against a table, see Table arguments ( #433 ) ?columnname__date=yyyy-mm-dd filter which returns rows where the spoecified datetime column falls on the specified date ( 583b22a ) ?tags__arraycontains=tag filter which acts against a JSON array contained in a column ( 78e45ea ) ?_where=sql-fragment filter for the table view ( #429 ) ?_fts_table=mytable and ?_fts_pk=mycolumn query string options can be used to specify which FTS table to use for a search query - see Configuring full-text search for a table or view ( #428 ) You can now pass the same table filter multiple times - for example, ?content__not=world&content__not=hello will return all rows where the content column is neither hello or world ( #288 ) You can now specify about and about_url metadata (in addition to source and license ) linking to further information about a project - see Source, license and about New ?_trace=1 parameter now adds debug information showing every SQL query that was executed while constructing the page ( #435 ) datasette inspect now just calculates table counts, and does not introspect other database metadata ( #462 ) Removed /-/inspect page entirely - this will be replaced by something similar in the future, see #465 Datasette can now run against an in-memory SQLite database. You can do this by starting it without passing any files or by using the new --memory option to datasette serve . This can be useful for experimenting with SQLite queries that do not access any data, such as SELECT 1+1 or SELECT sqlite_version() .","[""Changelog"", ""0.28 (2019-05-19)""]","[{""href"": ""https://github.com/python/black"", ""label"": ""Black coding style""}, {""href"": ""https://github.com/simonw/datasette/pull/449"", ""label"": ""#449""}, {""href"": ""https://github.com/simonw/datasette/issues/433"", ""label"": ""#433""}, {""href"": ""https://github.com/simonw/datasette/commit/583b22aa28e26c318de0189312350ab2688c90b1"", ""label"": ""583b22a""}, {""href"": ""https://github.com/simonw/datasette/commit/78e45ead4d771007c57b307edf8fc920101f8733"", ""label"": ""78e45ea""}, {""href"": ""https://github.com/simonw/datasette/issues/429"", ""label"": ""#429""}, {""href"": ""https://github.com/simonw/datasette/issues/428"", ""label"": ""#428""}, {""href"": ""https://github.com/simonw/datasette/issues/288"", ""label"": ""#288""}, {""href"": ""https://github.com/simonw/datasette/issues/435"", ""label"": ""#435""}, {""href"": ""https://github.com/simonw/datasette/issues/462"", ""label"": ""#462""}, {""href"": ""https://github.com/simonw/datasette/issues/465"", ""label"": ""#465""}]" internals:internals-csrf,internals,internals-csrf,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 <form method=""POST""> anywhere you will need to include that token. You can do so with the following template snippet: <input type=""hidden"" name=""csrftoken"" value=""{{ csrftoken() }}""> If you are rendering templates using the await .render_template(template, context=None, request=None) method the csrftoken() helper will only work if you provide the request= argument to that method. If you forget to do this you will see the following error: form-urlencoded POST field did not match cookie You can selectively disable CSRF protection using the skip_csrf(datasette, scope) hook.","[""Internals for plugins""]","[{""href"": ""https://github.com/simonw/asgi-csrf"", ""label"": ""asgi-csrf""}]" full_text_search:configuring-fts-using-csvs-to-sqlite,full_text_search,configuring-fts-using-csvs-to-sqlite,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","[""Full-text search"", ""Enabling full-text search for a SQLite table""]","[{""href"": ""https://github.com/simonw/csvs-to-sqlite"", ""label"": ""csvs-to-sqlite""}]" publish:cli-publish,publish,cli-publish,datasette publish,"Once you have created a SQLite database (e.g. using csvs-to-sqlite ) you can deploy it to a hosting account using a single command. You will need a hosting account with Heroku or Google Cloud . Once you have created your account you will need to install and configure the heroku or gcloud command-line tools.","[""Publishing data""]","[{""href"": ""https://github.com/simonw/csvs-to-sqlite/"", ""label"": ""csvs-to-sqlite""}, {""href"": ""https://www.heroku.com/"", ""label"": ""Heroku""}, {""href"": ""https://cloud.google.com/"", ""label"": ""Google Cloud""}]" changelog:id62,changelog,id62,0.43 (2020-05-28),"The main focus of this release is a major upgrade to the register_output_renderer(datasette) plugin hook, which allows plugins to provide new output formats for Datasette such as datasette-atom and datasette-ics . Redesign of register_output_renderer(datasette) to provide more context to the render callback and support an optional ""can_render"" callback that controls if a suggested link to the output format is provided. ( #581 , #770 ) Visually distinguish float and integer columns - useful for figuring out why order-by-column might be returning unexpected results. ( #729 ) The Request object , which is passed to several plugin hooks, is now documented. ( #706 ) New metadata.json option for setting a custom default page size for specific tables and views, see Setting a custom page size . ( #751 ) Canned queries can now be configured with a default URL fragment hash, useful when working with plugins such as datasette-vega , see Additional canned query options . ( #706 ) Fixed a bug in datasette publish when running on operating systems where the /tmp directory lives in a different volume, using a backport of the Python 3.8 shutil.copytree() function. ( #744 ) Every plugin hook is now covered by the unit tests, and a new unit test checks that each plugin hook has at least one corresponding test. ( #771 , #773 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette-atom"", ""label"": ""datasette-atom""}, {""href"": ""https://github.com/simonw/datasette-ics"", ""label"": ""datasette-ics""}, {""href"": ""https://github.com/simonw/datasette/issues/581"", ""label"": ""#581""}, {""href"": ""https://github.com/simonw/datasette/issues/770"", ""label"": ""#770""}, {""href"": ""https://github.com/simonw/datasette/issues/729"", ""label"": ""#729""}, {""href"": ""https://github.com/simonw/datasette/issues/706"", ""label"": ""#706""}, {""href"": ""https://github.com/simonw/datasette/issues/751"", ""label"": ""#751""}, {""href"": ""https://github.com/simonw/datasette-vega"", ""label"": ""datasette-vega""}, {""href"": ""https://github.com/simonw/datasette/issues/706"", ""label"": ""#706""}, {""href"": ""https://github.com/simonw/datasette/issues/744"", ""label"": ""#744""}, {""href"": ""https://github.com/simonw/datasette/issues/771"", ""label"": ""#771""}, {""href"": ""https://github.com/simonw/datasette/issues/773"", ""label"": ""#773""}]" changelog:secret-plugin-configuration-options,changelog,secret-plugin-configuration-options,Secret plugin configuration options,"Plugins like datasette-auth-github need a safe way to set secret configuration options. Since the default mechanism for configuring plugins exposes those settings in /-/metadata a new mechanism was needed. Secret configuration values describes how plugins can now specify that their settings should be read from a file or an environment variable: { ""plugins"": { ""datasette-auth-github"": { ""client_secret"": { ""$env"": ""GITHUB_CLIENT_SECRET"" } } } } These plugin secrets can be set directly using datasette publish . See Custom metadata and plugins for details. ( #538 and #543 )","[""Changelog"", ""0.29 (2019-07-07)""]","[{""href"": ""https://github.com/simonw/datasette-auth-github"", ""label"": ""datasette-auth-github""}, {""href"": ""https://github.com/simonw/datasette/issues/538"", ""label"": ""#538""}, {""href"": ""https://github.com/simonw/datasette/issues/543"", ""label"": ""#543""}]" changelog:new-plugin-hook-extra-template-vars,changelog,new-plugin-hook-extra-template-vars,New plugin hook: extra_template_vars,"The extra_template_vars(template, database, table, columns, view_name, request, datasette) plugin hook allows plugins to inject their own additional variables into the Datasette template context. This can be used in conjunction with custom templates to customize the Datasette interface. datasette-auth-github uses this hook to add custom HTML to the new top navigation bar (which is designed to be modified by plugins, see #540 ).","[""Changelog"", ""0.29 (2019-07-07)""]","[{""href"": ""https://github.com/simonw/datasette-auth-github"", ""label"": ""datasette-auth-github""}, {""href"": ""https://github.com/simonw/datasette/issues/540"", ""label"": ""#540""}]" changelog:authentication,changelog,authentication,Authentication,"Prior to this release the Datasette ecosystem has treated authentication as exclusively the realm of plugins, most notably through datasette-auth-github . 0.44 introduces Authentication and permissions as core Datasette concepts ( #699 ). This enables different plugins to share responsibility for authenticating requests - you might have one plugin that handles user accounts and another one that allows automated access via API keys, for example. You'll need to install plugins if you want full user accounts, but default Datasette can now authenticate a single root user with the new --root command-line option, which outputs a one-time use URL to authenticate as a root actor ( #784 ): datasette fixtures.db --root http://127.0.0.1:8001/-/auth-token?token=5b632f8cd44b868df625f5a6e2185d88eea5b22237fd3cc8773f107cc4fd6477 INFO: Started server process [14973] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8001 (Press CTRL+C to quit) Plugins can implement new ways of authenticating users using the new actor_from_request(datasette, request) hook.","[""Changelog"", ""0.44 (2020-06-11)""]","[{""href"": ""https://github.com/simonw/datasette-auth-github"", ""label"": ""datasette-auth-github""}, {""href"": ""https://github.com/simonw/datasette/issues/699"", ""label"": ""#699""}, {""href"": ""https://github.com/simonw/datasette/issues/784"", ""label"": ""#784""}]" authentication:authentication-root,authentication,authentication-root,"Using the ""root"" actor","Datasette currently leaves almost all forms of authentication to plugins - datasette-auth-github for example. The one exception is the ""root"" account, which you can sign into while using Datasette on your local machine. This provides access to a small number of debugging features. To sign in as root, start Datasette using the --root command-line option, like this: datasette --root http://127.0.0.1:8001/-/auth-token?token=786fc524e0199d70dc9a581d851f466244e114ca92f33aa3b42a139e9388daa7 INFO: Started server process [25801] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8001 (Press CTRL+C to quit) The URL on the first line includes a one-use token which can be used to sign in as the ""root"" actor in your browser. Click on that link and then visit http://127.0.0.1:8001/-/actor to confirm that you are authenticated as an actor that looks like this: { ""id"": ""root"" }","[""Authentication and permissions"", ""Actors""]","[{""href"": ""https://github.com/simonw/datasette-auth-github"", ""label"": ""datasette-auth-github""}]" writing_plugins:writing-plugins-static-assets,writing_plugins,writing-plugins-static-assets,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.","[""Writing plugins""]","[{""href"": ""https://github.com/simonw/datasette-cluster-map"", ""label"": ""datasette-cluster-map""}]" writing_plugins:writing-plugins-custom-templates,writing_plugins,writing-plugins-custom-templates,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.","[""Writing plugins""]","[{""href"": ""https://github.com/simonw/datasette-edit-schema"", ""label"": ""datasette-edit-schema""}]" writing_plugins:writing-plugins-extra-hooks,writing_plugins,writing-plugins-extra-hooks,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.","[""Writing plugins""]","[{""href"": ""https://github.com/simonw/datasette-graphql"", ""label"": ""datasette-graphql""}, {""href"": ""https://github.com/simonw/datasette-graphql/blob/main/README.md#adding-custom-fields-with-plugins"", ""label"": ""described here""}, {""href"": ""https://pluggy.readthedocs.io/en/stable/#specs"", ""label"": ""Pluggy specifications""}]" changelog:new-plugin-hooks,changelog,new-plugin-hooks,New plugin hooks,"register_magic_parameters(datasette) can be used to define new types of magic canned query parameters. startup(datasette) can run custom code when Datasette first starts up. datasette-init is a new plugin that uses this hook to create database tables and views on startup if they have not yet been created. ( #834 ) canned_queries(datasette, database, actor) lets plugins provide additional canned queries beyond those defined in Datasette's metadata. See datasette-saved-queries for an example of this hook in action. ( #852 ) forbidden(datasette, request, message) is a hook for customizing how Datasette responds to 403 forbidden errors. ( #812 )","[""Changelog"", ""0.45 (2020-07-01)""]","[{""href"": ""https://github.com/simonw/datasette-init"", ""label"": ""datasette-init""}, {""href"": ""https://github.com/simonw/datasette/issues/834"", ""label"": ""#834""}, {""href"": ""https://github.com/simonw/datasette-saved-queries"", ""label"": ""datasette-saved-queries""}, {""href"": ""https://github.com/simonw/datasette/issues/852"", ""label"": ""#852""}, {""href"": ""https://github.com/simonw/datasette/issues/812"", ""label"": ""#812""}]" authentication:authentication-permissions-instance,authentication,authentication-permissions-instance,Access to an instance,"Here's how to restrict access to your entire Datasette instance to just the ""id"": ""root"" user: [[[cog from metadata_doc import config_example config_example(cog, """""" title: My private Datasette instance allow: id: root """""") ]]] [[[end]]] To deny access to all users, you can use ""allow"": false : [[[cog config_example(cog, """""" title: My entirely inaccessible instance allow: false """""") ]]] [[[end]]] One reason to do this is if you are using a Datasette plugin - such as datasette-permissions-sql - to control permissions instead.","[""Authentication and permissions"", ""Access permissions in ""]","[{""href"": ""https://github.com/simonw/datasette-permissions-sql"", ""label"": ""datasette-permissions-sql""}]" writing_plugins:writing-plugins-cookiecutter,writing_plugins,writing-plugins-cookiecutter,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.","[""Writing plugins""]","[{""href"": ""https://github.com/simonw/datasette-plugin"", ""label"": ""datasette-plugin""}, {""href"": ""https://cookiecutter.readthedocs.io/en/stable/installation.html"", ""label"": ""Install cookiecutter""}, {""href"": ""https://simonwillison.net/2020/Jun/20/cookiecutter-plugins/"", ""label"": ""a cookiecutter template for writing Datasette plugins""}]" writing_plugins:writing-plugins-packaging,writing_plugins,writing-plugins-packaging,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 .","[""Writing plugins""]","[{""href"": ""https://github.com/simonw/datasette-plugin-demos"", ""label"": ""https://github.com/simonw/datasette-plugin-demos""}, {""href"": ""https://pypi.org/"", ""label"": ""PyPI""}, {""href"": ""https://packaging.python.org/tutorials/distributing-packages/"", ""label"": ""Packaging and distributing projects""}]" binary_data:binary-plugins,binary_data,binary-plugins,Binary plugins,"Several Datasette plugins are available that change the way Datasette treats binary data. datasette-render-binary modifies Datasette's default interface to show an automatic guess at what type of binary data is being stored, along with a visual representation of the binary value that displays ASCII strings directly in the interface. datasette-render-images detects common image formats and renders them as images directly in the Datasette interface. datasette-media allows Datasette interfaces to be configured to serve binary files from configured SQL queries, and includes the ability to resize images directly before serving them.","[""Binary data""]","[{""href"": ""https://github.com/simonw/datasette-render-binary"", ""label"": ""datasette-render-binary""}, {""href"": ""https://github.com/simonw/datasette-render-images"", ""label"": ""datasette-render-images""}, {""href"": ""https://github.com/simonw/datasette-media"", ""label"": ""datasette-media""}]" custom_templates:custom-pages-parameters,custom_templates,custom-pages-parameters,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 .","[""Custom pages and templates""]","[{""href"": ""https://github.com/simonw/datasette-template-sql"", ""label"": ""datasette-template-sql""}, {""href"": ""https://github.com/simonw/datasette-graphql#the-graphql-template-function"", ""label"": ""datasette-graphql""}]" authentication:authentication-permissions,authentication,authentication-permissions,Permissions,"Datasette has an extensive permissions system built-in, which can be further extended and customized by plugins. The key question the permissions system answers is this: Is this actor allowed to perform this action , optionally against this particular resource ? Actors are described above . An action is a string describing the action the actor would like to perform. A full list is provided below - examples include view-table and execute-sql . A resource is the item the actor wishes to interact with - for example a specific database or table. Some actions, such as permissions-debug , are not associated with a particular resource. Datasette's built-in view permissions ( view-database , view-table etc) default to allow - unless you configure additional permission rules unauthenticated users will be allowed to access content. Permissions with potentially harmful effects should default to deny . Plugin authors should account for this when designing new plugins - for example, the datasette-upload-csvs plugin defaults to deny so that installations don't accidentally allow unauthenticated users to create new tables by uploading a CSV file.","[""Authentication and permissions""]","[{""href"": ""https://github.com/simonw/datasette-upload-csvs"", ""label"": ""datasette-upload-csvs""}]" changelog:plugins-can-now-add-links-within-datasette,changelog,plugins-can-now-add-links-within-datasette,Plugins can now add links within Datasette,"A number of existing Datasette plugins add new pages to the Datasette interface, providig tools for things like uploading CSVs , editing table schemas or configuring full-text search . Plugins like this can now link to themselves from other parts of Datasette interface. The menu_links(datasette, actor, request) hook ( #1064 ) lets plugins add links to Datasette's new top-right application menu, and the table_actions(datasette, actor, database, table, request) hook ( #1066 ) adds links to a new ""table actions"" menu on the table page. The demo at latest.datasette.io now includes some example plugins. To see the new table actions menu first sign into that demo as root and then visit the facetable table to see the new cog icon menu at the top of the page.","[""Changelog"", ""0.51 (2020-10-31)""]","[{""href"": ""https://github.com/simonw/datasette-upload-csvs"", ""label"": ""uploading CSVs""}, {""href"": ""https://github.com/simonw/datasette-edit-schema"", ""label"": ""editing table schemas""}, {""href"": ""https://github.com/simonw/datasette-configure-fts"", ""label"": ""configuring full-text search""}, {""href"": ""https://github.com/simonw/datasette/issues/1064"", ""label"": ""#1064""}, {""href"": ""https://github.com/simonw/datasette/issues/1066"", ""label"": ""#1066""}, {""href"": ""https://latest.datasette.io/"", ""label"": ""latest.datasette.io""}, {""href"": ""https://latest.datasette.io/login-as-root"", ""label"": ""sign into that demo as root""}, {""href"": ""https://latest.datasette.io/fixtures/facetable"", ""label"": ""facetable""}]" publish:publish-custom-metadata-and-plugins,publish,publish-custom-metadata-and-plugins,Custom metadata and plugins,"datasette publish accepts a number of additional options which can be used to further customize your Datasette instance. You can define your own Metadata and deploy that with your instance like so: datasette publish cloudrun --service=my-service mydatabase.db -m metadata.json If you just want to set the title, license or source information you can do that directly using extra options to datasette publish : datasette publish cloudrun mydatabase.db --service=my-service \ --title=""Title of my database"" \ --source=""Where the data originated"" \ --source_url=""http://www.example.com/"" You can also specify plugins you would like to install. For example, if you want to include the datasette-vega visualization plugin you can use the following: datasette publish cloudrun mydatabase.db --service=my-service --install=datasette-vega If a plugin has any Secret configuration values you can use the --plugin-secret option to set those secrets at publish time. For example, using Heroku with datasette-auth-github you might run the following command: datasette publish heroku my_database.db \ --name my-heroku-app-demo \ --install=datasette-auth-github \ --plugin-secret datasette-auth-github client_id your_client_id \ --plugin-secret datasette-auth-github client_secret your_client_secret","[""Publishing data"", ""datasette publish""]","[{""href"": ""https://github.com/simonw/datasette-vega"", ""label"": ""datasette-vega""}, {""href"": ""https://github.com/simonw/datasette-auth-github"", ""label"": ""datasette-auth-github""}]" sql_queries:fragment,sql_queries,fragment,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.","[""Running SQL queries"", ""Canned queries"", ""Additional canned query options""]","[{""href"": ""https://github.com/simonw/datasette-vega"", ""label"": ""datasette-vega""}, {""href"": ""https://latest.datasette.io/fixtures#queries"", ""label"": ""See here""}]" contributing:contributing-release,contributing,contributing-release,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.","[""Contributing""]","[{""href"": ""https://github.com/simonw/datasette/blob/main/.github/workflows/deploy-latest.yml"", ""label"": ""GitHub Action workflow""}, {""href"": ""https://hub.docker.com/r/datasetteproject/datasette"", ""label"": ""https://hub.docker.com/r/datasetteproject/datasette""}, {""href"": ""https://pypi.org/project/datasette/"", ""label"": ""https://pypi.org/project/datasette/""}, {""href"": ""https://readthedocs.org/projects/datasette/versions/"", ""label"": ""https://readthedocs.org/projects/datasette/versions/""}, {""href"": ""https://semver.org/"", ""label"": ""Semantic Versioning""}, {""href"": ""https://github.com/simonw/datasette/commit/0e1e89c6ba3d0fbdb0823272952cf356f3016def"", ""label"": ""commit can be seen here""}, {""href"": ""https://github.com/simonw/datasette/issues/581#ref-commit-d56f402"", ""label"": ""here""}, {""href"": ""https://observablehq.com/@simonw/extract-issue-numbers-from-pasted-text"", ""label"": ""Extract issue numbers from pasted text""}, {""href"": ""https://github.com/simonw/datasette/releases/new"", ""label"": ""a new release""}, {""href"": ""https://euangoddard.github.io/clipboard2markdown/"", ""label"": ""Paste to Markdown tool""}, {""href"": ""https://datasette.io/"", ""label"": ""datasette.io""}, {""href"": ""https://github.com/simonw/datasette.io/blob/main/news.yaml"", ""label"": ""news.yaml""}]" custom_templates:customization-custom-templates,custom_templates,customization-custom-templates,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 <body> 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: <!-- Templates considered: *query-mydb-tz.html, query-mydb.html, query.html --> 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 %} <h1>EXTRA HTML AT THE TOP OF THE CONTENT BLOCK</h1> <p>This line renders the original block:</p> {{ 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 %} <div> <h2>{{ row[""title""] }}</h2> <p>{{ row[""description""] }}<lp> <p>Category: {{ row.display(""category_id"") }}</p> </div> {% endfor %}","[""Custom pages and templates"", ""Publishing static assets""]","[{""href"": ""https://github.com/simonw/datasette/blob/main/datasette/templates/_table.html"", ""label"": ""can be seen here""}]" custom_templates:custom-pages-errors,custom_templates,custom-pages-errors,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.","[""Custom pages and templates"", ""Custom redirects""]","[{""href"": ""https://github.com/simonw/datasette/blob/main/datasette/templates/error.html"", ""label"": ""default error.html template""}]" changelog:id114,changelog,id114,0.23 (2018-06-18),"This release features CSV export, improved options for foreign key expansions, new configuration settings and improved support for SpatiaLite. See datasette/compare/0.22.1...0.23 for a full list of commits added since the last release.","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/compare/0.22.1...0.23"", ""label"": ""datasette/compare/0.22.1...0.23""}]" changelog:id56,changelog,id56,0.47 (2020-08-11),"Datasette now has a GitHub discussions forum for conversations about the project that go beyond just bug reports and issues. Datasette can now be installed on macOS using Homebrew! Run brew install simonw/datasette/datasette . See Using Homebrew . ( #335 ) Two new commands: datasette install name-of-plugin and datasette uninstall name-of-plugin . These are equivalent to pip install and pip uninstall but automatically run in the same virtual environment as Datasette, so users don't have to figure out where that virtual environment is - useful for installations created using Homebrew or pipx . See Installing plugins . ( #925 ) A new command-line option, datasette --get , accepts a path to a URL within the Datasette instance. It will run that request through Datasette (without starting a web server) and print out the response. See datasette --get for an example. ( #926 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/discussions"", ""label"": ""a GitHub discussions forum""}, {""href"": ""https://github.com/simonw/datasette/issues/335"", ""label"": ""#335""}, {""href"": ""https://github.com/simonw/datasette/issues/925"", ""label"": ""#925""}, {""href"": ""https://github.com/simonw/datasette/issues/926"", ""label"": ""#926""}]" contributing:contributing-alpha-beta,contributing,contributing-alpha-beta,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.","[""Contributing""]","[{""href"": ""https://github.com/simonw/datasette/issues"", ""label"": ""discussions on the issue tracker""}]" changelog:id48,changelog,id48,0.50.1 (2020-10-09),"Fixed a bug introduced in 0.50 where the export as JSON/CSV links on the table, row and query pages were broken. ( #1010 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1010"", ""label"": ""#1010""}]" changelog:id47,changelog,id47,0.50.2 (2020-10-09),Fixed another bug introduced in 0.50 where column header links on the table page were broken. ( #1011 ),"[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1011"", ""label"": ""#1011""}]" changelog:running-datasette-behind-a-proxy,changelog,running-datasette-behind-a-proxy,Running Datasette behind a proxy,"The base_url configuration option is designed to help run Datasette on a specific path behind a proxy - for example if you want to run an instance of Datasette at /my-datasette/ within your existing site's URL hierarchy, proxied behind nginx or Apache. Support for this configuration option has been greatly improved ( #1023 ), and guidelines for using it are now available in a new documentation section on Running Datasette behind a proxy . ( #1027 )","[""Changelog"", ""0.51 (2020-10-31)""]","[{""href"": ""https://github.com/simonw/datasette/issues/1023"", ""label"": ""#1023""}, {""href"": ""https://github.com/simonw/datasette/issues/1027"", ""label"": ""#1027""}]" changelog:binary-data,changelog,binary-data,Binary data,"SQLite tables can contain binary data in BLOB columns. Datasette now provides links for users to download this data directly from Datasette, and uses those links to make binary data available from CSV exports. See Binary data for more details. ( #1036 and #1034 ).","[""Changelog"", ""0.51 (2020-10-31)""]","[{""href"": ""https://github.com/simonw/datasette/issues/1036"", ""label"": ""#1036""}, {""href"": ""https://github.com/simonw/datasette/issues/1034"", ""label"": ""#1034""}]" changelog:id198,changelog,id198,0.12 (2017-11-16),"Added __version__ , now displayed as tooltip in page footer ( #108 ). Added initial docs, including a changelog ( #99 ). Turned on auto-escaping in Jinja. Added a UI for editing named parameters ( #96 ). You can now construct a custom SQL statement using SQLite named parameters (e.g. :name ) and datasette will display form fields for editing those parameters. Here’s an example which lets you see the most popular names for dogs of different species registered through various dog registration schemes in Australia. Pin to specific Jinja version. ( #100 ). Default to 127.0.0.1 not 0.0.0.0. ( #98 ). Added extra metadata options to publish and package commands. ( #92 ). You can now run these commands like so: datasette now publish mydb.db \ --title=""My Title"" \ --source=""Source"" \ --source_url=""http://www.example.com/"" \ --license=""CC0"" \ --license_url=""https://creativecommons.org/publicdomain/zero/1.0/"" This will write those values into the metadata.json that is packaged with the app. If you also pass --metadata=metadata.json that file will be updated with the extra values before being written into the Docker image. Added production-ready Dockerfile ( #94 ) [Andrew Cutler] New ?_sql_time_limit_ms=10 argument to database and table page ( #95 ) SQL syntax highlighting with Codemirror ( #89 ) [Tom Dyson]","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/108"", ""label"": ""#108""}, {""href"": ""https://github.com/simonw/datasette/issues/99"", ""label"": ""#99""}, {""href"": ""https://github.com/simonw/datasette/issues/96"", ""label"": ""#96""}, {""href"": ""https://australian-dogs.now.sh/australian-dogs-3ba9628?sql=select+name%2C+count%28*%29+as+n+from+%28%0D%0A%0D%0Aselect+upper%28%22Animal+name%22%29+as+name+from+%5BAdelaide-City-Council-dog-registrations-2013%5D+where+Breed+like+%3Abreed%0D%0A%0D%0Aunion+all%0D%0A%0D%0Aselect+upper%28Animal_Name%29+as+name+from+%5BAdelaide-City-Council-dog-registrations-2014%5D+where+Breed_Description+like+%3Abreed%0D%0A%0D%0Aunion+all+%0D%0A%0D%0Aselect+upper%28Animal_Name%29+as+name+from+%5BAdelaide-City-Council-dog-registrations-2015%5D+where+Breed_Description+like+%3Abreed%0D%0A%0D%0Aunion+all%0D%0A%0D%0Aselect+upper%28%22AnimalName%22%29+as+name+from+%5BCity-of-Port-Adelaide-Enfield-Dog_Registrations_2016%5D+where+AnimalBreed+like+%3Abreed%0D%0A%0D%0Aunion+all%0D%0A%0D%0Aselect+upper%28%22Animal+Name%22%29+as+name+from+%5BMitcham-dog-registrations-2015%5D+where+Breed+like+%3Abreed%0D%0A%0D%0Aunion+all%0D%0A%0D%0Aselect+upper%28%22DOG_NAME%22%29+as+name+from+%5Bburnside-dog-registrations-2015%5D+where+DOG_BREED+like+%3Abreed%0D%0A%0D%0Aunion+all+%0D%0A%0D%0Aselect+upper%28%22Animal_Name%22%29+as+name+from+%5Bcity-of-playford-2015-dog-registration%5D+where+Breed_Description+like+%3Abreed%0D%0A%0D%0Aunion+all%0D%0A%0D%0Aselect+upper%28%22Animal+Name%22%29+as+name+from+%5Bcity-of-prospect-dog-registration-details-2016%5D+where%22Breed+Description%22+like+%3Abreed%0D%0A%0D%0A%29+group+by+name+order+by+n+desc%3B&breed=pug"", ""label"": ""Here’s an example""}, {""href"": ""https://github.com/simonw/datasette/issues/100"", ""label"": ""#100""}, {""href"": ""https://github.com/simonw/datasette/issues/98"", ""label"": ""#98""}, {""href"": ""https://github.com/simonw/datasette/issues/92"", ""label"": ""#92""}, {""href"": ""https://github.com/simonw/datasette/issues/94"", ""label"": ""#94""}, {""href"": ""https://github.com/simonw/datasette/issues/95"", ""label"": ""#95""}, {""href"": ""https://github.com/simonw/datasette/issues/89"", ""label"": ""#89""}]" changelog:id42,changelog,id42,0.52.1 (2020-11-29),"Documentation on Testing plugins now recommends using datasette.client . ( #1102 ) Fix bug where compound foreign keys produced broken links. ( #1098 ) datasette --load-module=spatialite now also checks for /usr/local/lib/mod_spatialite.so . Thanks, Dan Peterson. ( #1114 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1102"", ""label"": ""#1102""}, {""href"": ""https://github.com/simonw/datasette/issues/1098"", ""label"": ""#1098""}, {""href"": ""https://github.com/simonw/datasette/issues/1114"", ""label"": ""#1114""}]" changelog:id41,changelog,id41,0.52.2 (2020-12-02),"Generated columns from SQLite 3.31.0 or higher are now correctly displayed. ( #1116 ) Error message if you attempt to open a SpatiaLite database now suggests using --load-extension=spatialite if it detects that the extension is available in a common location. ( #1115 ) OPTIONS requests against the /database page no longer raise a 500 error. ( #1100 ) Databases larger than 32MB that are published to Cloud Run can now be downloaded. ( #749 ) Fix for misaligned cog icon on table and database pages. Thanks, Abdussamet KoΓ§ak. ( #1121 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1116"", ""label"": ""#1116""}, {""href"": ""https://github.com/simonw/datasette/issues/1115"", ""label"": ""#1115""}, {""href"": ""https://github.com/simonw/datasette/issues/1100"", ""label"": ""#1100""}, {""href"": ""https://github.com/simonw/datasette/issues/749"", ""label"": ""#749""}, {""href"": ""https://github.com/simonw/datasette/issues/1121"", ""label"": ""#1121""}]" changelog:id40,changelog,id40,0.52.3 (2020-12-03),Fixed bug where static assets would 404 for Datasette installed on ARM Amazon Linux. ( #1124 ),"[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1124"", ""label"": ""#1124""}]" changelog:id38,changelog,id38,0.52.5 (2020-12-09),Fix for error caused by combining the _searchmode=raw and ?_search_COLUMN parameters. ( #1134 ),"[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1134"", ""label"": ""#1134""}]" changelog:named-in-memory-database-support,changelog,named-in-memory-database-support,Named in-memory database support,"As part of the work building the _internal database, Datasette now supports named in-memory databases that can be shared across multiple connections. This allows plugins to create in-memory databases which will persist data for the lifetime of the Datasette server process. ( #1151 ) The new memory_name= parameter to the Database class can be used to create named, shared in-memory databases.","[""Changelog"", ""0.54 (2021-01-25)""]","[{""href"": ""https://github.com/simonw/datasette/issues/1151"", ""label"": ""#1151""}]" changelog:smaller-changes,changelog,smaller-changes,Smaller changes,"Datasette documentation now shows YAML examples for Metadata by default, with a tab interface for switching to JSON. ( #1153 ) register_output_renderer(datasette) plugins now have access to error and truncated arguments, allowing them to display error messages and take into account truncated results. ( #2130 ) render_cell() plugin hook now also supports an optional request argument. ( #2007 ) New Justfile to support development workflows for Datasette using Just . datasette.render_template() can now accepts a datasette.views.Context subclass as an alternative to a dictionary. ( #2127 ) datasette install -e path option for editable installations, useful while developing plugins. ( #2106 ) When started with the --cors option Datasette now serves an Access-Control-Max-Age: 3600 header, ensuring CORS OPTIONS requests are repeated no more than once an hour. ( #2079 ) Fixed a bug where the _internal database could display None instead of null for in-memory databases. ( #1970 )","[""Changelog"", ""1.0a3 (2023-08-09)""]","[{""href"": ""https://github.com/simonw/datasette/issues/1153"", ""label"": ""#1153""}, {""href"": ""https://github.com/simonw/datasette/issues/2130"", ""label"": ""#2130""}, {""href"": ""https://github.com/simonw/datasette/issues/2007"", ""label"": ""#2007""}, {""href"": ""https://github.com/casey/just"", ""label"": ""Just""}, {""href"": ""https://github.com/simonw/datasette/issues/2127"", ""label"": ""#2127""}, {""href"": ""https://github.com/simonw/datasette/issues/2106"", ""label"": ""#2106""}, {""href"": ""https://github.com/simonw/datasette/issues/2079"", ""label"": ""#2079""}, {""href"": ""https://github.com/simonw/datasette/issues/1970"", ""label"": ""#1970""}]" changelog:id34,changelog,id34,0.54.1 (2021-02-02),Fixed a bug where ?_search= and ?_sort= parameters were incorrectly duplicated when the filter form on the table page was re-submitted. ( #1214 ),"[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1214"", ""label"": ""#1214""}]" changelog:id27,changelog,id27,0.58.1 (2021-07-16),Fix for an intermittent race condition caused by the refresh_schemas() internal function. ( #1231 ),"[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1231"", ""label"": ""#1231""}]" changelog:id32,changelog,id32,0.56 (2021-03-28),"Documentation improvements, bug fixes and support for SpatiaLite 5. The SQL editor can now be resized by dragging a handle. ( #1236 ) Fixed a bug with JSON faceting and the __arraycontains filter caused by tables with spaces in their names. ( #1239 ) Upgraded httpx dependency. ( #1005 ) JSON faceting is now suggested even if a column contains blank strings. ( #1246 ) New datasette.add_memory_database() method. ( #1247 ) The Response.asgi_send() method is now documented. ( #1266 ) The official Datasette Docker image now bundles SpatiaLite version 5. ( #1278 ) Fixed a no such table: pragma_database_list bug when running Datasette against SQLite versions prior to SQLite 3.16.0. ( #1276 ) HTML lists displayed in table cells are now styled correctly. Thanks, Bob Whitelock. ( #1141 , #1252 ) Configuration directory mode now correctly serves immutable databases that are listed in inspect-data.json . Thanks Campbell Allen and Frankie Robertson. ( #1031 , #1229 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1236"", ""label"": ""#1236""}, {""href"": ""https://github.com/simonw/datasette/issues/1239"", ""label"": ""#1239""}, {""href"": ""https://github.com/simonw/datasette/issues/1005"", ""label"": ""#1005""}, {""href"": ""https://github.com/simonw/datasette/issues/1246"", ""label"": ""#1246""}, {""href"": ""https://github.com/simonw/datasette/issues/1247"", ""label"": ""#1247""}, {""href"": ""https://github.com/simonw/datasette/issues/1266"", ""label"": ""#1266""}, {""href"": ""https://github.com/simonw/datasette/issues/1278"", ""label"": ""#1278""}, {""href"": ""https://github.com/simonw/datasette/issues/1276"", ""label"": ""#1276""}, {""href"": ""https://github.com/simonw/datasette/issues/1141"", ""label"": ""#1141""}, {""href"": ""https://github.com/simonw/datasette/pull/1252"", ""label"": ""#1252""}, {""href"": ""https://github.com/simonw/datasette/pull/1031"", ""label"": ""#1031""}, {""href"": ""https://github.com/simonw/datasette/pull/1229"", ""label"": ""#1229""}]" changelog:bug-fixes-and-other-improvements,changelog,bug-fixes-and-other-improvements,Bug fixes and other improvements,"Custom pages now work correctly when combined with the base_url setting. ( #1238 ) Fixed intermittent error displaying the index page when the user did not have permission to access one of the tables. Thanks, Guy Freeman. ( #1305 ) Columns with the name ""Link"" are no longer incorrectly displayed in bold. ( #1308 ) Fixed error caused by tables with a single quote in their names. ( #1257 ) Updated dependencies: pytest-asyncio , Black , jinja2 , aiofiles , click , and itsdangerous . The official Datasette Docker image now supports apt-get install . ( #1320 ) The Heroku runtime used by datasette publish heroku is now python-3.8.10 .","[""Changelog"", ""0.57 (2021-06-05)""]","[{""href"": ""https://github.com/simonw/datasette/issues/1238"", ""label"": ""#1238""}, {""href"": ""https://github.com/simonw/datasette/issues/1305"", ""label"": ""#1305""}, {""href"": ""https://github.com/simonw/datasette/issues/1308"", ""label"": ""#1308""}, {""href"": ""https://github.com/simonw/datasette/issues/1257"", ""label"": ""#1257""}, {""href"": ""https://github.com/simonw/datasette/issues/1320"", ""label"": ""#1320""}]" changelog:id183,changelog,id183,0.13 (2017-11-24),"Search now applies to current filters. Combined search into the same form as filters. Closes #133 Much tidier design for table view header. Closes #147 Added ?column__not=blah filter. Closes #148 Row page now resolves foreign keys. Closes #132 Further tweaks to select/input filter styling. Refs #86 - thanks for the help, @natbat! Show linked foreign key in table cells. Added UI for editing table filters. Refs #86 Hide FTS-created tables on index pages. Closes #129 Add publish to heroku support [Jacob Kaplan-Moss] datasette publish heroku mydb.db Pull request #104 Initial implementation of ?_group_count=column . URL shortcut for counting rows grouped by one or more columns. ?_group_count=column1&_group_count=column2 works as well. SQL generated looks like this: select ""qSpecies"", count(*) as ""count"" from Street_Tree_List group by ""qSpecies"" order by ""count"" desc limit 100 Or for two columns like this: select ""qSpecies"", ""qSiteInfo"", count(*) as ""count"" from Street_Tree_List group by ""qSpecies"", ""qSiteInfo"" order by ""count"" desc limit 100 Refs #44 Added --build=master option to datasette publish and package. The datasette publish and datasette package commands both now accept an optional --build argument. If provided, this can be used to specify a branch published to GitHub that should be built into the container. This makes it easier to test code that has not yet been officially released to PyPI, e.g.: datasette publish now mydb.db --branch=master Implemented ?_search=XXX + UI if a FTS table is detected. Closes #131 Added datasette --version support. Table views now show expanded foreign key references, if possible. If a table has foreign key columns, and those foreign key tables have label_columns , the TableView will now query those other tables for the corresponding values and display those values as links in the corresponding table cells. label_columns are currently detected by the inspect() function, which looks for any table that has just two columns - an ID column and one other - and sets the label_column to be that second non-ID column. Don't prevent tabbing to ""Run SQL"" button ( #117 ) [Robert Gieseke] See comment in #115 Add keyboard shortcut to execute SQL query ( #115 ) [Robert Gieseke] Allow --load-extension to be set via environment variable. Add support for ?field__isnull=1 ( #107 ) [Ray N] Add spatialite, switch to debian and local build ( #114 ) [Ariel NΓΊΓ±ez] Added --load-extension argument to datasette serve. Allows loading of SQLite extensions. Refs #110 .","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/133"", ""label"": ""#133""}, {""href"": ""https://github.com/simonw/datasette/issues/147"", ""label"": ""#147""}, {""href"": ""https://github.com/simonw/datasette/issues/148"", ""label"": ""#148""}, {""href"": ""https://github.com/simonw/datasette/issues/132"", ""label"": ""#132""}, {""href"": ""https://github.com/simonw/datasette/issues/86"", ""label"": ""#86""}, {""href"": ""https://github.com/simonw/datasette/issues/86"", ""label"": ""#86""}, {""href"": ""https://github.com/simonw/datasette/issues/129"", ""label"": ""#129""}, {""href"": ""https://github.com/simonw/datasette/issues/104"", ""label"": ""#104""}, {""href"": ""https://github.com/simonw/datasette/issues/44"", ""label"": ""#44""}, {""href"": ""https://github.com/simonw/datasette/issues/131"", ""label"": ""#131""}, {""href"": ""https://github.com/simonw/datasette/issues/117"", ""label"": ""#117""}, {""href"": ""https://github.com/simonw/datasette/issues/115"", ""label"": ""#115""}, {""href"": ""https://github.com/simonw/datasette/issues/115"", ""label"": ""#115""}, {""href"": ""https://github.com/simonw/datasette/issues/107"", ""label"": ""#107""}, {""href"": ""https://github.com/simonw/datasette/issues/114"", ""label"": ""#114""}, {""href"": ""https://github.com/simonw/datasette/issues/110"", ""label"": ""#110""}]" changelog:id29,changelog,id29,0.57.1 (2021-06-08),"Fixed visual display glitch with global navigation menu. ( #1367 ) No longer truncates the list of table columns displayed on the /database page. ( #1364 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1367"", ""label"": ""#1367""}, {""href"": ""https://github.com/simonw/datasette/issues/1364"", ""label"": ""#1364""}]" plugin_hooks:plugin-hook-get-metadata,plugin_hooks,plugin-hook-get-metadata,"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","[""Plugin hooks""]","[{""href"": ""https://github.com/simonw/datasette/issues/1384"", ""label"": ""issue 1384""}, {""href"": ""https://datasette.io/plugins/datasette-remote-metadata"", ""label"": ""datasette-remote-metadata plugin""}]" changelog:id28,changelog,id28,0.58 (2021-07-14),"New datasette --uds /tmp/datasette.sock option for binding Datasette to a Unix domain socket, see proxy documentation ( #1388 ) ""searchmode"": ""raw"" table metadata option for defaulting a table to executing SQLite full-text search syntax without first escaping it, see Advanced SQLite search queries . ( #1389 ) New plugin hook: get_metadata(datasette, key, database, table) , for returning custom metadata for an instance, database or table. Thanks, Brandon Roberts! ( #1384 ) New plugin hook: skip_csrf(datasette, scope) , for opting out of CSRF protection based on the incoming request. ( #1377 ) The menu_links() , table_actions() and database_actions() plugin hooks all gained a new optional request argument providing access to the current request. ( #1371 ) Major performance improvement for Datasette faceting. ( #1394 ) Improved documentation for Running Datasette behind a proxy to recommend using ProxyPreservehost On with Apache. ( #1387 ) POST requests to endpoints that do not support that HTTP verb now return a 405 error. db.path can now be provided as a pathlib.Path object, useful when writing unit tests for plugins. Thanks, Chris Amico. ( #1365 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1388"", ""label"": ""#1388""}, {""href"": ""https://github.com/simonw/datasette/issues/1389"", ""label"": ""#1389""}, {""href"": ""https://github.com/simonw/datasette/issues/1384"", ""label"": ""#1384""}, {""href"": ""https://github.com/simonw/datasette/issues/1377"", ""label"": ""#1377""}, {""href"": ""https://github.com/simonw/datasette/issues/1371"", ""label"": ""#1371""}, {""href"": ""https://github.com/simonw/datasette/issues/1394"", ""label"": ""#1394""}, {""href"": ""https://github.com/simonw/datasette/issues/1387"", ""label"": ""#1387""}, {""href"": ""https://github.com/simonw/datasette/issues/1365"", ""label"": ""#1365""}]" changelog:id7,changelog,id7,0.64 (2023-01-09),"Datasette now strongly recommends against allowing arbitrary SQL queries if you are using SpatiaLite . SpatiaLite includes SQL functions that could cause the Datasette server to crash. See SpatiaLite for more details. New default_allow_sql setting, providing an easier way to disable all arbitrary SQL execution by end users: datasette --setting default_allow_sql off . See also Controlling the ability to execute arbitrary SQL . ( #1409 ) Building a location to time zone API with SpatiaLite is a new Datasette tutorial showing how to safely use SpatiaLite to create a location to time zone API. New documentation about how to debug problems loading SQLite extensions . The error message shown when an extension cannot be loaded has also been improved. ( #1979 ) Fixed an accessibility issue: the <select> elements in the table filter form now show an outline when they are currently focused. ( #1771 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1409"", ""label"": ""#1409""}, {""href"": ""https://datasette.io/tutorials/spatialite"", ""label"": ""Building a location to time zone API with SpatiaLite""}, {""href"": ""https://github.com/simonw/datasette/issues/1979"", ""label"": ""#1979""}, {""href"": ""https://github.com/simonw/datasette/issues/1771"", ""label"": ""#1771""}]" changelog:id25,changelog,id25,0.59.1 (2021-10-24),"Fix compatibility with Python 3.10. ( #1482 ) Documentation on how to use Named parameters with integer and floating point values. ( #1496 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1482"", ""label"": ""#1482""}, {""href"": ""https://github.com/simonw/datasette/issues/1496"", ""label"": ""#1496""}]" changelog:id24,changelog,id24,0.59.2 (2021-11-13),"Column names with a leading underscore now work correctly when used as a facet. ( #1506 ) Applying ?_nocol= to a column no longer removes that column from the filtering interface. ( #1503 ) Official Datasette Docker container now uses Debian Bullseye as the base image. ( #1497 ) Datasette is four years old today! Here's the original release announcement from 2017.","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1506"", ""label"": ""#1506""}, {""href"": ""https://github.com/simonw/datasette/issues/1503"", ""label"": ""#1503""}, {""href"": ""https://github.com/simonw/datasette/issues/1497"", ""label"": ""#1497""}, {""href"": ""https://simonwillison.net/2017/Nov/13/datasette/"", ""label"": ""original release announcement""}]" changelog:id22,changelog,id22,0.59.4 (2021-11-29),"Fixed bug where columns with a leading underscore could not be removed from the interactive filters list. ( #1527 ) Fixed bug where columns with a leading underscore were not correctly linked to by the ""Links from other tables"" interface on the row page. ( #1525 ) Upgraded dependencies aiofiles , black and janus .","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1527"", ""label"": ""#1527""}, {""href"": ""https://github.com/simonw/datasette/issues/1525"", ""label"": ""#1525""}]" changelog:other-small-fixes,changelog,other-small-fixes,Other small fixes,"Made several performance improvements to the database schema introspection code that runs when Datasette first starts up. ( #1555 ) Label columns detected for foreign keys are now case-insensitive, so Name or TITLE will be detected in the same way as name or title . ( #1544 ) Upgraded Pluggy dependency to 1.0. ( #1575 ) Now using Plausible analytics for the Datasette documentation. explain query plan is now allowed with varying amounts of whitespace in the query. ( #1588 ) New CLI reference page showing the output of --help for each of the datasette sub-commands. This lead to several small improvements to the help copy. ( #1594 ) Fixed bug where writable canned queries could not be used with custom templates. ( #1547 ) Improved fix for a bug where columns with a underscore prefix could result in unnecessary hidden form fields. ( #1527 )","[""Changelog"", ""0.60 (2022-01-13)""]","[{""href"": ""https://github.com/simonw/datasette/issues/1555"", ""label"": ""#1555""}, {""href"": ""https://github.com/simonw/datasette/issues/1544"", ""label"": ""#1544""}, {""href"": ""https://github.com/simonw/datasette/issues/1575"", ""label"": ""#1575""}, {""href"": ""https://plausible.io/"", ""label"": ""Plausible analytics""}, {""href"": ""https://github.com/simonw/datasette/issues/1588"", ""label"": ""#1588""}, {""href"": ""https://github.com/simonw/datasette/issues/1594"", ""label"": ""#1594""}, {""href"": ""https://github.com/simonw/datasette/issues/1547"", ""label"": ""#1547""}, {""href"": ""https://github.com/simonw/datasette/issues/1527"", ""label"": ""#1527""}]" changelog:faceting,changelog,faceting,Faceting,"The number of unique values in a facet is now always displayed. Previously it was only displayed if the user specified ?_facet_size=max . ( #1556 ) Facets of type date or array can now be configured in metadata.json , see Facets in metadata . Thanks, David Larlet. ( #1552 ) New ?_nosuggest=1 parameter for table views, which disables facet suggestion. ( #1557 ) Fixed bug where ?_facet_array=tags&_facet=tags would only display one of the two selected facets. ( #625 )","[""Changelog"", ""0.60 (2022-01-13)""]","[{""href"": ""https://github.com/simonw/datasette/issues/1556"", ""label"": ""#1556""}, {""href"": ""https://github.com/simonw/datasette/issues/1552"", ""label"": ""#1552""}, {""href"": ""https://github.com/simonw/datasette/issues/1557"", ""label"": ""#1557""}, {""href"": ""https://github.com/simonw/datasette/issues/625"", ""label"": ""#625""}]" changelog:id20,changelog,id20,0.60.1 (2022-01-20),"Fixed a bug where installation on Python 3.6 stopped working due to a change to an underlying dependency. This release can now be installed on Python 3.6, but is the last release of Datasette that will support anything less than Python 3.7. ( #1609 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1609"", ""label"": ""#1609""}]" changelog:id19,changelog,id19,0.60.2 (2022-02-07),Fixed a bug where Datasette would open the same file twice with two different database names if you ran datasette file.db file.db . ( #1632 ),"[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1632"", ""label"": ""#1632""}]" changelog:id18,changelog,id18,0.61 (2022-03-23),"In preparation for Datasette 1.0, this release includes two potentially backwards-incompatible changes. Hashed URL mode has been moved to a separate plugin, and the way Datasette generates URLs to databases and tables with special characters in their name such as / and . has changed. Datasette also now requires Python 3.7 or higher. URLs within Datasette now use a different encoding scheme for tables or databases that include ""special"" characters outside of the range of a-zA-Z0-9_- . This scheme is explained here: Tilde encoding . ( #1657 ) Removed hashed URL mode from Datasette. The new datasette-hashed-urls plugin can be used to achieve the same result, see datasette-hashed-urls for details. ( #1661 ) Databases can now have a custom path within the Datasette instance that is independent of the database name, using the db.route property. ( #1668 ) Datasette is now covered by a Code of Conduct . ( #1654 ) Python 3.6 is no longer supported. ( #1577 ) Tests now run against Python 3.11-dev. ( #1621 ) New datasette.ensure_permissions(actor, permissions) internal method for checking multiple permissions at once. ( #1675 ) New datasette.check_visibility(actor, action, resource=None) internal method for checking if a user can see a resource that would otherwise be invisible to unauthenticated users. ( #1678 ) Table and row HTML pages now include a <link rel=""alternate"" type=""application/json+datasette"" href=""...""> element and return a Link: URL; rel=""alternate""; type=""application/json+datasette"" HTTP header pointing to the JSON version of those pages. ( #1533 ) Access-Control-Expose-Headers: Link is now added to the CORS headers, allowing remote JavaScript to access that header. Canned queries are now shown at the top of the database page, directly below the SQL editor. Previously they were shown at the bottom, below the list of tables. ( #1612 ) Datasette now has a default favicon. ( #1603 ) sqlite_stat tables are now hidden by default. ( #1587 ) SpatiaLite tables data_licenses , KNN and KNN2 are now hidden by default. ( #1601 ) SQL query tracing mechanism now works for queries executed in asyncio sub-tasks, such as those created by asyncio.gather() . ( #1576 ) datasette.tracer mechanism is now documented. Common Datasette symbols can now be imported directly from the top-level datasette package, see Import shortcuts . Those symbols are Response , Forbidden , NotFound , hookimpl , actor_matches_allow . ( #957 ) /-/versions page now returns additional details for libraries used by SpatiaLite. ( #1607 ) Documentation now links to the Datasette Tutorials . Datasette will now also look for SpatiaLite in /opt/homebrew - thanks, Dan Peterson. ( #1649 ) Fixed bug where custom pages did not work on Windows. Thanks, Robert Christie. ( #1545 ) Fixed error caused when a table had a column named n . ( #1228 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1657"", ""label"": ""#1657""}, {""href"": ""https://github.com/simonw/datasette/issues/1661"", ""label"": ""#1661""}, {""href"": ""https://github.com/simonw/datasette/issues/1668"", ""label"": ""#1668""}, {""href"": ""https://github.com/simonw/datasette/blob/main/CODE_OF_CONDUCT.md"", ""label"": ""Code of Conduct""}, {""href"": ""https://github.com/simonw/datasette/issues/1654"", ""label"": ""#1654""}, {""href"": ""https://github.com/simonw/datasette/issues/1577"", ""label"": ""#1577""}, {""href"": ""https://github.com/simonw/datasette/issues/1621"", ""label"": ""#1621""}, {""href"": ""https://github.com/simonw/datasette/issues/1675"", ""label"": ""#1675""}, {""href"": ""https://github.com/simonw/datasette/issues/1678"", ""label"": ""#1678""}, {""href"": ""https://github.com/simonw/datasette/issues/1533"", ""label"": ""#1533""}, {""href"": ""https://github.com/simonw/datasette/issues/1612"", ""label"": ""#1612""}, {""href"": ""https://github.com/simonw/datasette/issues/1603"", ""label"": ""#1603""}, {""href"": ""https://github.com/simonw/datasette/issues/1587"", ""label"": ""#1587""}, {""href"": ""https://github.com/simonw/datasette/issues/1601"", ""label"": ""#1601""}, {""href"": ""https://github.com/simonw/datasette/issues/1576"", ""label"": ""#1576""}, {""href"": ""https://github.com/simonw/datasette/issues/957"", ""label"": ""#957""}, {""href"": ""https://github.com/simonw/datasette/issues/1607"", ""label"": ""#1607""}, {""href"": ""https://datasette.io/tutorials"", ""label"": ""Datasette Tutorials""}, {""href"": ""https://github.com/simonw/datasette/pull/1649"", ""label"": ""#1649""}, {""href"": ""https://github.com/simonw/datasette/issues/1545"", ""label"": ""#1545""}, {""href"": ""https://github.com/simonw/datasette/issues/1228"", ""label"": ""#1228""}]" changelog:bug-fixes,changelog,bug-fixes,Bug fixes,"Don't show the facet option in the cog menu if faceting is not allowed. ( #1683 ) ?_sort and ?_sort_desc now work if the column that is being sorted has been excluded from the query using ?_col= or ?_nocol= . ( #1773 ) Fixed bug where ?_sort_desc was duplicated in the URL every time the Apply button was clicked. ( #1738 )","[""Changelog"", ""0.62 (2022-08-14)""]","[{""href"": ""https://github.com/simonw/datasette/issues/1683"", ""label"": ""#1683""}, {""href"": ""https://github.com/simonw/datasette/issues/1773"", ""label"": ""#1773""}, {""href"": ""https://github.com/simonw/datasette/issues/1738"", ""label"": ""#1738""}]" changelog:id86,changelog,id86,Small changes,"We now show the size of the database file next to the download link ( #172 ) New /-/databases introspection page shows currently connected databases ( #470 ) Binary data is no longer displayed on the table and row pages ( #442 - thanks, Russ Garrett) New show/hide SQL links on custom query pages ( #415 ) The extra_body_script plugin hook now accepts an optional view_name argument ( #443 - thanks, Russ Garrett) Bumped Jinja2 dependency to 2.10.1 ( #426 ) All table filters are now documented, and documentation is enforced via unit tests ( 2c19a27 ) New project guideline: master should stay shippable at all times! ( 31f36e1 ) Fixed a bug where sqlite_timelimit() occasionally failed to clean up after itself ( bac4e01 ) We no longer load additional plugins when executing pytest ( #438 ) Homepage now links to database views if there are less than five tables in a database ( #373 ) The --cors option is now respected by error pages ( #453 ) datasette publish heroku now uses the --include-vcs-ignore option, which means it works under Travis CI ( #407 ) datasette publish heroku now publishes using Python 3.6.8 ( 666c374 ) Renamed datasette publish now to datasette publish nowv1 ( #472 ) datasette publish nowv1 now accepts multiple --alias parameters ( 09ef305 ) Removed the datasette skeleton command ( #476 ) The documentation on how to build the documentation now recommends sphinx-autobuild","[""Changelog"", ""0.28 (2019-05-19)""]","[{""href"": ""https://github.com/simonw/datasette/issues/172"", ""label"": ""#172""}, {""href"": ""https://github.com/simonw/datasette/issues/470"", ""label"": ""#470""}, {""href"": ""https://github.com/simonw/datasette/pull/442"", ""label"": ""#442""}, {""href"": ""https://github.com/simonw/datasette/issues/415"", ""label"": ""#415""}, {""href"": ""https://github.com/simonw/datasette/pull/443"", ""label"": ""#443""}, {""href"": ""https://github.com/simonw/datasette/pull/426"", ""label"": ""#426""}, {""href"": ""https://github.com/simonw/datasette/commit/2c19a27d15a913e5f3dd443f04067169a6f24634"", ""label"": ""2c19a27""}, {""href"": ""https://github.com/simonw/datasette/commit/31f36e1b97ccc3f4387c80698d018a69798b6228"", ""label"": ""31f36e1""}, {""href"": ""https://github.com/simonw/datasette/commit/bac4e01f40ae7bd19d1eab1fb9349452c18de8f5"", ""label"": ""bac4e01""}, {""href"": ""https://github.com/simonw/datasette/issues/438"", ""label"": ""#438""}, {""href"": ""https://github.com/simonw/datasette/issues/373"", ""label"": ""#373""}, {""href"": ""https://github.com/simonw/datasette/issues/453"", ""label"": ""#453""}, {""href"": ""https://github.com/simonw/datasette/pull/407"", ""label"": ""#407""}, {""href"": ""https://github.com/simonw/datasette/commit/666c37415a898949fae0437099d62a35b1e9c430"", ""label"": ""666c374""}, {""href"": ""https://github.com/simonw/datasette/issues/472"", ""label"": ""#472""}, {""href"": ""https://github.com/simonw/datasette/commit/09ef305c687399384fe38487c075e8669682deb4"", ""label"": ""09ef305""}, {""href"": ""https://github.com/simonw/datasette/issues/476"", ""label"": ""#476""}]" changelog:id16,changelog,id16,Documentation,"Examples in the documentation now include a copy-to-clipboard button. ( #1748 ) Documentation now uses the Furo Sphinx theme. ( #1746 ) Code examples in the documentation are now all formatted using Black. ( #1718 ) Request.fake() method is now documented, see Request object . New documentation for plugin authors: Registering a plugin for the duration of a test . ( #903 )","[""Changelog"", ""0.62 (2022-08-14)""]","[{""href"": ""https://github.com/simonw/datasette/issues/1748"", ""label"": ""#1748""}, {""href"": ""https://github.com/pradyunsg/furo"", ""label"": ""Furo""}, {""href"": ""https://github.com/simonw/datasette/issues/1746"", ""label"": ""#1746""}, {""href"": ""https://github.com/simonw/datasette/issues/1718"", ""label"": ""#1718""}, {""href"": ""https://github.com/simonw/datasette/issues/903"", ""label"": ""#903""}]" changelog:id15,changelog,id15,Plugin hooks,"New plugin hook: handle_exception() , for custom handling of exceptions caught by Datasette. ( #1770 ) The render_cell() plugin hook is now also passed a row argument, representing the sqlite3.Row object that is being rendered. ( #1300 ) The configuration directory is now stored in datasette.config_dir , making it available to plugins. Thanks, Chris Amico. ( #1766 )","[""Changelog"", ""0.62 (2022-08-14)""]","[{""href"": ""https://github.com/simonw/datasette/issues/1770"", ""label"": ""#1770""}, {""href"": ""https://github.com/simonw/datasette/issues/1300"", ""label"": ""#1300""}, {""href"": ""https://github.com/simonw/datasette/pull/1766"", ""label"": ""#1766""}]" changelog:plugin-hooks-and-internals,changelog,plugin-hooks-and-internals,Plugin hooks and internals,"The prepare_jinja2_environment(env, datasette) plugin hook now accepts an optional datasette argument. Hook implementations can also now return an async function which will be awaited automatically. ( #1809 ) Database(is_mutable=) now defaults to True . ( #1808 ) The datasette.check_visibility() method now accepts an optional permissions= list, allowing it to take multiple permissions into account at once when deciding if something should be shown as public or private. This has been used to correctly display padlock icons in more places in the Datasette interface. ( #1829 ) Datasette no longer enforces upper bounds on its dependencies. ( #1800 )","[""Changelog"", ""0.63 (2022-10-27)""]","[{""href"": ""https://github.com/simonw/datasette/issues/1809"", ""label"": ""#1809""}, {""href"": ""https://github.com/simonw/datasette/issues/1808"", ""label"": ""#1808""}, {""href"": ""https://github.com/simonw/datasette/issues/1829"", ""label"": ""#1829""}, {""href"": ""https://github.com/simonw/datasette/issues/1800"", ""label"": ""#1800""}]" changelog:documentation,changelog,documentation,Documentation,"Documentation describing how to write tests that use signed actor cookies using datasette.client.actor_cookie() . ( #1830 ) Documentation on how to register a plugin for the duration of a test . ( #2234 ) The configuration documentation now shows examples of both YAML and JSON for each setting.","[""Changelog"", ""1.0a8 (2024-02-07)""]","[{""href"": ""https://github.com/simonw/datasette/issues/1830"", ""label"": ""#1830""}, {""href"": ""https://github.com/simonw/datasette/issues/2234"", ""label"": ""#2234""}]" changelog:v1-0-a0,changelog,v1-0-a0,1.0a0 (2022-11-29),"This first alpha release of Datasette 1.0 introduces a brand new collection of APIs for writing to the database ( #1850 ), as well as a new API token mechanism baked into Datasette core. Previously, API tokens have only been supported by installing additional plugins. This is very much a preview: expect many more backwards incompatible API changes prior to the full 1.0 release. Feedback enthusiastically welcomed, either through issue comments or via the Datasette Discord community.","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1850"", ""label"": ""#1850""}, {""href"": ""https://github.com/simonw/datasette/issues/1850"", ""label"": ""issue comments""}, {""href"": ""https://datasette.io/discord"", ""label"": ""Datasette Discord""}]" changelog:signed-api-tokens,changelog,signed-api-tokens,Signed API tokens,"New /-/create-token page allowing authenticated users to create signed API tokens that can act on their behalf, see API Tokens . ( #1852 ) New datasette create-token command for creating tokens from the command line: datasette create-token . New allow_signed_tokens setting which can be used to turn off signed token support. ( #1856 ) New max_signed_tokens_ttl setting for restricting the maximum allowed duration of a signed token. ( #1858 )","[""Changelog"", ""1.0a0 (2022-11-29)""]","[{""href"": ""https://github.com/simonw/datasette/issues/1852"", ""label"": ""#1852""}, {""href"": ""https://github.com/simonw/datasette/issues/1856"", ""label"": ""#1856""}, {""href"": ""https://github.com/simonw/datasette/issues/1858"", ""label"": ""#1858""}]" changelog:features,changelog,features,Features,"Now tested against Python 3.11. Docker containers used by datasette publish and datasette package both now use that version of Python. ( #1853 ) --load-extension option now supports entrypoints. Thanks, Alex Garcia. ( #1789 ) Facet size can now be set per-table with the new facet_size table metadata option. ( #1804 ) The truncate_cells_html setting now also affects long URLs in columns. ( #1805 ) The non-JavaScript SQL editor textarea now increases height to fit the SQL query. ( #1786 ) Facets are now displayed with better line-breaks in long values. Thanks, Daniel Rech. ( #1794 ) The settings.json file used in Configuration directory mode is now validated on startup. ( #1816 ) SQL queries can now include leading SQL comments, using /* ... */ or -- ... syntax. Thanks, Charles Nepote. ( #1860 ) SQL query is now re-displayed when terminated with a time limit error. ( #1819 ) The inspect data mechanism is now used to speed up server startup - thanks, Forest Gregg. ( #1834 ) In Configuration directory mode databases with filenames ending in .sqlite or .sqlite3 are now automatically added to the Datasette instance. ( #1646 ) Breadcrumb navigation display now respects the current user's permissions. ( #1831 )","[""Changelog"", ""0.63 (2022-10-27)""]","[{""href"": ""https://github.com/simonw/datasette/issues/1853"", ""label"": ""#1853""}, {""href"": ""https://github.com/simonw/datasette/pull/1789"", ""label"": ""#1789""}, {""href"": ""https://github.com/simonw/datasette/issues/1804"", ""label"": ""#1804""}, {""href"": ""https://github.com/simonw/datasette/issues/1805"", ""label"": ""#1805""}, {""href"": ""https://github.com/simonw/datasette/issues/1786"", ""label"": ""#1786""}, {""href"": ""https://github.com/simonw/datasette/pull/1794"", ""label"": ""#1794""}, {""href"": ""https://github.com/simonw/datasette/issues/1816"", ""label"": ""#1816""}, {""href"": ""https://github.com/simonw/datasette/issues/1860"", ""label"": ""#1860""}, {""href"": ""https://github.com/simonw/datasette/issues/1819"", ""label"": ""#1819""}, {""href"": ""https://github.com/simonw/datasette/issues/1834"", ""label"": ""#1834""}, {""href"": ""https://github.com/simonw/datasette/issues/1646"", ""label"": ""#1646""}, {""href"": ""https://github.com/simonw/datasette/issues/1831"", ""label"": ""#1831""}]" changelog:write-api,changelog,write-api,Write API,"New API explorer at /-/api for trying out the API. ( #1871 ) /db/-/create API for Creating a table . ( #1882 ) /db/table/-/insert API for Inserting rows . ( #1851 ) /db/table/-/drop API for Dropping tables . ( #1874 ) /db/table/pk/-/update API for Updating a row . ( #1863 ) /db/table/pk/-/delete API for Deleting a row . ( #1864 )","[""Changelog"", ""1.0a0 (2022-11-29)""]","[{""href"": ""https://github.com/simonw/datasette/issues/1871"", ""label"": ""#1871""}, {""href"": ""https://github.com/simonw/datasette/issues/1882"", ""label"": ""#1882""}, {""href"": ""https://github.com/simonw/datasette/issues/1851"", ""label"": ""#1851""}, {""href"": ""https://github.com/simonw/datasette/issues/1874"", ""label"": ""#1874""}, {""href"": ""https://github.com/simonw/datasette/issues/1863"", ""label"": ""#1863""}, {""href"": ""https://github.com/simonw/datasette/issues/1864"", ""label"": ""#1864""}]" changelog:id10,changelog,id10,0.63.1 (2022-11-10),"Fixed a bug where Datasette's table filter form would not redirect correctly when run behind a proxy using the base_url setting. ( #1883 ) SQL query is now shown wrapped in a <textarea> if a query exceeds a time limit. ( #1876 ) Fixed an intermittent ""Too many open files"" error while running the test suite. ( #1843 ) New db.close() internal method.","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1883"", ""label"": ""#1883""}, {""href"": ""https://github.com/simonw/datasette/issues/1876"", ""label"": ""#1876""}, {""href"": ""https://github.com/simonw/datasette/issues/1843"", ""label"": ""#1843""}]" changelog:id9,changelog,id9,0.63.2 (2022-11-18),"Fixed a bug in datasette publish heroku where deployments failed due to an older version of Python being requested. ( #1905 ) New datasette publish heroku --generate-dir <dir> option for generating a Heroku deployment directory without deploying it.","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1905"", ""label"": ""#1905""}]" changelog:v1-0-a1,changelog,v1-0-a1,1.0a1 (2022-12-01),"Write APIs now serve correct CORS headers if Datasette is started in --cors mode. See the full list of CORS headers in the documentation. ( #1922 ) Fixed a bug where the _memory database could be written to even though writes were not persisted. ( #1917 ) The https://latest.datasette.io/ demo instance now includes an ephemeral database which can be used to test Datasette's write APIs, using the new datasette-ephemeral-tables plugin to drop any created tables after five minutes. This database is only available if you sign in as the root user using the link on the homepage. ( #1915 ) Fixed a bug where hitting the write endpoints with a GET request returned a 500 error. It now returns a 405 (method not allowed) error instead. ( #1916 ) The list of endpoints in the API explorer now lists mutable databases first. ( #1918 ) The ""ignore"": true and ""replace"": true options for the insert API are now documented . ( #1924 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1922"", ""label"": ""#1922""}, {""href"": ""https://github.com/simonw/datasette/issues/1917"", ""label"": ""#1917""}, {""href"": ""https://latest.datasette.io/"", ""label"": ""https://latest.datasette.io/""}, {""href"": ""https://datasette.io/plugins/datasette-ephemeral-tables"", ""label"": ""datasette-ephemeral-tables""}, {""href"": ""https://github.com/simonw/datasette/issues/1915"", ""label"": ""#1915""}, {""href"": ""https://github.com/simonw/datasette/issues/1916"", ""label"": ""#1916""}, {""href"": ""https://github.com/simonw/datasette/issues/1918"", ""label"": ""#1918""}, {""href"": ""https://github.com/simonw/datasette/issues/1924"", ""label"": ""#1924""}]" changelog:id155,changelog,id155,0.16 (2018-04-13),"Better mechanism for handling errors; 404s for missing table/database New error mechanism closes #193 404s for missing tables/databases closes #184 long_description in markdown for the new PyPI Hide SpatiaLite system tables. [Russ Garrett] Allow explain select / explain query plan select #201 Datasette inspect now finds primary_keys #195 Ability to sort using form fields (for mobile portrait mode) #199 We now display sort options as a select box plus a descending checkbox, which means you can apply sort orders even in portrait mode on a mobile phone where the column headers are hidden.","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/193"", ""label"": ""#193""}, {""href"": ""https://github.com/simonw/datasette/issues/184"", ""label"": ""#184""}, {""href"": ""https://github.com/simonw/datasette/issues/201"", ""label"": ""#201""}, {""href"": ""https://github.com/simonw/datasette/issues/195"", ""label"": ""#195""}, {""href"": ""https://github.com/simonw/datasette/issues/199"", ""label"": ""#199""}]" changelog:id161,changelog,id161,0.15 (2018-04-09),"The biggest new feature in this release is the ability to sort by column. On the table page the column headers can now be clicked to apply sort (or descending sort), or you can specify ?_sort=column or ?_sort_desc=column directly in the URL. table_rows => table_rows_count , filtered_table_rows => filtered_table_rows_count Renamed properties. Closes #194 New sortable_columns option in metadata.json to control sort options. You can now explicitly set which columns in a table can be used for sorting using the _sort and _sort_desc arguments using metadata.json : { ""databases"": { ""database1"": { ""tables"": { ""example_table"": { ""sortable_columns"": [ ""height"", ""weight"" ] } } } } } Refs #189 Column headers now link to sort/desc sort - refs #189 _sort and _sort_desc parameters for table views Allows for paginated sorted results based on a specified column. Refs #189 Total row count now correct even if _next applied Use .custom_sql() for _group_count implementation (refs #150 ) Make HTML title more readable in query template ( #180 ) [Ryan Pitts] New ?_shape=objects/object/lists param for JSON API ( #192 ) New _shape= parameter replacing old .jsono extension Now instead of this: /database/table.jsono We use the _shape parameter like this: /database/table.json?_shape=objects Also introduced a new _shape called object which looks like this: /database/table.json?_shape=object Returning an object for the rows key: ... ""rows"": { ""pk1"": { ... }, ""pk2"": { ... } } Refs #122 Utility for writing test database fixtures to a .db file python tests/fixtures.py /tmp/hello.db This is useful for making a SQLite database of the test fixtures for interactive exploration. Compound primary key _next= now plays well with extra filters Closes #190 Fixed bug with keyset pagination over compound primary keys Refs #190 Database/Table views inherit source/license/source_url/license_url metadata If you set the source_url/license_url/source/license fields in your root metadata those values will now be inherited all the way down to the database and table templates. The title/description are NOT inherited. Also added unit tests for the HTML generated by the metadata. Refs #185 Add metadata, if it exists, to heroku temp dir ( #178 ) [Tony Hirst] Initial documentation for pagination Broke up test_app into test_api and test_html Fixed bug with .json path regular expression I had a table called geojson and it caused an exception because the regex was matching .json and not \.json Deploy to Heroku with Python 3.6.3","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/194"", ""label"": ""#194""}, {""href"": ""https://github.com/simonw/datasette/issues/189"", ""label"": ""#189""}, {""href"": ""https://github.com/simonw/datasette/issues/189"", ""label"": ""#189""}, {""href"": ""https://github.com/simonw/datasette/issues/189"", ""label"": ""#189""}, {""href"": ""https://github.com/simonw/datasette/issues/150"", ""label"": ""#150""}, {""href"": ""https://github.com/simonw/datasette/issues/180"", ""label"": ""#180""}, {""href"": ""https://github.com/simonw/datasette/issues/192"", ""label"": ""#192""}, {""href"": ""https://github.com/simonw/datasette/issues/122"", ""label"": ""#122""}, {""href"": ""https://github.com/simonw/datasette/issues/190"", ""label"": ""#190""}, {""href"": ""https://github.com/simonw/datasette/issues/190"", ""label"": ""#190""}, {""href"": ""https://github.com/simonw/datasette/issues/185"", ""label"": ""#185""}, {""href"": ""https://github.com/simonw/datasette/issues/178"", ""label"": ""#178""}]" changelog:id8,changelog,id8,0.63.3 (2022-12-17),"Fixed a bug where datasette --root , when running in Docker, would only output the URL to sign in root when the server shut down, not when it started up. ( #1958 ) You no longer need to ensure await datasette.invoke_startup() has been called in order for Datasette to start correctly serving requests - this is now handled automatically the first time the server receives a request. This fixes a bug experienced when Datasette is served directly by an ASGI application server such as Uvicorn or Gunicorn. It also fixes a bug with the datasette-gunicorn plugin. ( #1955 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1958"", ""label"": ""#1958""}, {""href"": ""https://datasette.io/plugins/datasette-gunicorn"", ""label"": ""datasette-gunicorn""}, {""href"": ""https://github.com/simonw/datasette/issues/1955"", ""label"": ""#1955""}]" changelog:id6,changelog,id6,0.64.1 (2023-01-11),"Documentation now links to a current source of information for installing Python 3. ( #1987 ) Incorrectly calling the Datasette constructor using Datasette(""path/to/data.db"") instead of Datasette([""path/to/data.db""]) now returns a useful error message. ( #1985 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/1987"", ""label"": ""#1987""}, {""href"": ""https://github.com/simonw/datasette/issues/1985"", ""label"": ""#1985""}]" changelog:id5,changelog,id5,0.64.2 (2023-03-08),"Fixed a bug with datasette publish cloudrun where deploys all used the same Docker image tag. This was mostly inconsequential as the service is deployed as soon as the image has been pushed to the registry, but could result in the incorrect image being deployed if two different deploys for two separate services ran at exactly the same time. ( #2036 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/2036"", ""label"": ""#2036""}]" changelog:configuration,changelog,configuration,Configuration,"Plugin configuration now lives in the datasette.yaml configuration file , passed to Datasette using the -c/--config option. Thanks, Alex Garcia. ( #2093 ) datasette -c datasette.yaml Where datasette.yaml contains configuration that looks like this: plugins: datasette-cluster-map: latitude_column: xlat longitude_column: xlon Previously plugins were configured in metadata.yaml , which was confusing as plugin settings were unrelated to database and table metadata. The -s/--setting option can now be used to set plugin configuration as well. See Configuration via the command-line for details. ( #2252 ) The above YAML configuration example using -s/--setting looks like this: datasette mydatabase.db \ -s plugins.datasette-cluster-map.latitude_column xlat \ -s plugins.datasette-cluster-map.longitude_column xlon The new /-/config page shows the current instance configuration, after redacting keys that could contain sensitive data such as API keys or passwords. ( #2254 ) Existing Datasette installations may already have configuration set in metadata.yaml that should be migrated to datasette.yaml . To avoid breaking these installations, Datasette will silently treat table configuration, plugin configuration and allow blocks in metadata as if they had been specified in configuration instead. ( #2247 ) ( #2248 ) ( #2249 ) Note that the datasette publish command has not yet been updated to accept a datasette.yaml configuration file. This will be addressed in #2195 but for the moment you can include those settings in metadata.yaml instead.","[""Changelog"", ""1.0a8 (2024-02-07)""]","[{""href"": ""https://github.com/simonw/datasette/issues/2093"", ""label"": ""#2093""}, {""href"": ""https://github.com/simonw/datasette/issues/2252"", ""label"": ""#2252""}, {""href"": ""https://github.com/simonw/datasette/issues/2254"", ""label"": ""#2254""}, {""href"": ""https://github.com/simonw/datasette/issues/2247"", ""label"": ""#2247""}, {""href"": ""https://github.com/simonw/datasette/issues/2248"", ""label"": ""#2248""}, {""href"": ""https://github.com/simonw/datasette/issues/2249"", ""label"": ""#2249""}, {""href"": ""https://github.com/simonw/datasette/issues/2195"", ""label"": ""#2195""}]" changelog:alter-table-support-for-create-insert-upsert-and-update,changelog,alter-table-support-for-create-insert-upsert-and-update,"Alter table support for create, insert, upsert and update","The JSON write API can now be used to apply simple alter table schema changes, provided the acting actor has the new alter-table permission. ( #2101 ) The only alter operation supported so far is adding new columns to an existing table. The /db/-/create API now adds new columns during large operations to create a table based on incoming example ""rows"" , in the case where one of the later rows includes columns that were not present in the earlier batches. This requires the create-table but not the alter-table permission. When /db/-/create is called with rows in a situation where the table may have been already created, an ""alter"": true key can be included to indicate that any missing columns from the new rows should be added to the table. This requires the alter-table permission. /db/table/-/insert and /db/table/-/upsert and /db/table/row-pks/-/update all now also accept ""alter"": true , depending on the alter-table permission. Operations that alter a table now fire the new alter-table event .","[""Changelog"", ""1.0a9 (2024-02-16)""]","[{""href"": ""https://github.com/simonw/datasette/issues/2101"", ""label"": ""#2101""}]" changelog:v1-0-a5,changelog,v1-0-a5,1.0a5 (2023-08-29),"When restrictions are applied to API tokens , those restrictions now behave slightly differently: applying the view-table restriction will imply the ability to view-database for the database containing that table, and both view-table and view-database will imply view-instance . Previously you needed to create a token with restrictions that explicitly listed view-instance and view-database and view-table in order to view a table without getting a permission denied error. ( #2102 ) New datasette.yaml (or .json ) configuration file, which can be specified using datasette -c path-to-file . The goal here to consolidate settings, plugin configuration, permissions, canned queries, and other Datasette configuration into a single single file, separate from metadata.yaml . The legacy settings.json config file used for Configuration directory mode has been removed, and datasette.yaml has a ""settings"" section where the same settings key/value pairs can be included. In the next future alpha release, more configuration such as plugins/permissions/canned queries will be moved to the datasette.yaml file. See #2093 for more details. Thanks, Alex Garcia. The -s/--setting option can now take dotted paths to nested settings. These will then be used to set or over-ride the same options as are present in the new configuration file. ( #2156 ) New --actor '{""id"": ""json-goes-here""}' option for use with datasette --get to treat the simulated request as being made by a specific actor, see datasette --get . ( #2153 ) The Datasette _internal database has had some changes. It no longer shows up in the datasette.databases list by default, and is now instead available to plugins using the datasette.get_internal_database() . Plugins are invited to use this as a private database to store configuration and settings and secrets that should not be made visible through the default Datasette interface. Users can pass the new --internal internal.db option to persist that internal database to disk. Thanks, Alex Garcia. ( #2157 ).","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/2102"", ""label"": ""#2102""}, {""href"": ""https://github.com/simonw/datasette/issues/2093"", ""label"": ""#2093""}, {""href"": ""https://github.com/simonw/datasette/issues/2156"", ""label"": ""#2156""}, {""href"": ""https://github.com/simonw/datasette/issues/2153"", ""label"": ""#2153""}, {""href"": ""https://github.com/simonw/datasette/issues/2157"", ""label"": ""#2157""}]" changelog:v1-0-a6,changelog,v1-0-a6,1.0a6 (2023-09-07),"New plugin hook: actors_from_ids(datasette, actor_ids) and an internal method to accompany it, await .actors_from_ids(actor_ids) . This mechanism is intended to be used by plugins that may need to display the actor who was responsible for something managed by that plugin: they can now resolve the recorded IDs of actors into the full actor objects. ( #2181 ) DATASETTE_LOAD_PLUGINS environment variable for controlling which plugins are loaded by Datasette. ( #2164 ) Datasette now checks if the user has permission to view a table linked to by a foreign key before turning that foreign key into a clickable link. ( #2178 ) The execute-sql permission now implies that the actor can also view the database and instance. ( #2169 ) Documentation describing a pattern for building plugins that themselves define further hooks for other plugins. ( #1765 ) Datasette is now tested against the Python 3.12 preview. ( #2175 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/2181"", ""label"": ""#2181""}, {""href"": ""https://github.com/simonw/datasette/issues/2164"", ""label"": ""#2164""}, {""href"": ""https://github.com/simonw/datasette/issues/2178"", ""label"": ""#2178""}, {""href"": ""https://github.com/simonw/datasette/issues/2169"", ""label"": ""#2169""}, {""href"": ""https://github.com/simonw/datasette/issues/1765"", ""label"": ""#1765""}, {""href"": ""https://github.com/simonw/datasette/pull/2175"", ""label"": ""#2175""}]" changelog:minor-fixes,changelog,minor-fixes,Minor fixes,"Datasette no longer attempts to run SQL queries in parallel when rendering a table page, as this was leading to some rare crashing bugs. ( #2189 ) Fixed warning: DeprecationWarning: pkg_resources is deprecated as an API ( #2057 ) Fixed bug where ?_extra=columns parameter returned an incorrectly shaped response. ( #2230 )","[""Changelog"", ""1.0a8 (2024-02-07)""]","[{""href"": ""https://github.com/simonw/datasette/issues/2189"", ""label"": ""#2189""}, {""href"": ""https://github.com/simonw/datasette/issues/2057"", ""label"": ""#2057""}, {""href"": ""https://github.com/simonw/datasette/issues/2230"", ""label"": ""#2230""}]" changelog:v1-0-a7,changelog,v1-0-a7,1.0a7 (2023-09-21),Fix for a crashing bug caused by viewing the table page for a named in-memory database. ( #2189 ),"[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/2189"", ""label"": ""#2189""}]" changelog:id4,changelog,id4,0.64.4 (2023-09-21),Fix for a crashing bug caused by viewing the table page for a named in-memory database. ( #2189 ),"[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/2189"", ""label"": ""#2189""}]" internals:internals-tracer-trace-child-tasks,internals,internals-tracer-trace-child-tasks,Tracing child tasks,"If your code uses a mechanism such as asyncio.gather() to execute code in additional tasks you may find that some of the traces are missing from the display. You can use the trace_child_tasks() context manager to ensure these child tasks are correctly handled. from datasette import tracer with tracer.trace_child_tasks(): results = await asyncio.gather( # ... async tasks here ) This example uses the register_routes() plugin hook to add a page at /parallel-queries which executes two SQL queries in parallel using asyncio.gather() and returns their results. from datasette import hookimpl from datasette import tracer @hookimpl def register_routes(): async def parallel_queries(datasette): db = datasette.get_database() with tracer.trace_child_tasks(): one, two = await asyncio.gather( db.execute(""select 1""), db.execute(""select 2""), ) return Response.json( { ""one"": one.single_value(), ""two"": two.single_value(), } ) return [ (r""/parallel-queries$"", parallel_queries), ] Note that running parallel SQL queries in this way has been known to cause problems in the past , so treat this example with caution. Adding ?_trace=1 will show that the trace covers both of those child tasks.","[""Internals for plugins"", ""datasette.tracer""]","[{""href"": ""https://github.com/simonw/datasette/issues/2189"", ""label"": ""been known to cause problems in the past""}]" changelog:id3,changelog,id3,0.64.5 (2023-10-08),"Dropped dependency on click-default-group-wheel , which could cause a dependency conflict. ( #2197 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/2197"", ""label"": ""#2197""}]" changelog:id2,changelog,id2,0.64.6 (2023-12-22),Fixed a bug where CSV export with expanded labels could fail if a foreign key reference did not correctly resolve. ( #2214 ),"[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/2214"", ""label"": ""#2214""}]" changelog:plugin-hooks,changelog,plugin-hooks,Plugin hooks,"New jinja2_environment_from_request(datasette, request, env) plugin hook, which can be used to customize the current Jinja environment based on the incoming request. This can be used to modify the template lookup path based on the incoming request hostname, among other things. ( #2225 ) New family of template slot plugin hooks : top_homepage , top_database , top_table , top_row , top_query , top_canned_query . Plugins can use these to provide additional HTML to be injected at the top of the corresponding pages. ( #1191 ) New track_event() mechanism for plugins to emit and receive events when certain events occur within Datasette. ( #2240 ) Plugins can register additional event classes using register_events(datasette) . They can then trigger those events with the datasette.track_event(event) internal method. Plugins can subscribe to notifications of events using the track_event(datasette, event) plugin hook. Datasette core now emits login , logout , create-token , create-table , drop-table , insert-rows , upsert-rows , update-row , delete-row events, documented here . New internal function for plugin authors: await db.execute_isolated_fn(fn) , for creating a new SQLite connection, executing code and then closing that connection, all while preventing other code from writing to that particular database. This connection will not have the prepare_connection() plugin hook executed against it, allowing plugins to perform actions that might otherwise be blocked by existing connection configuration. ( #2218 )","[""Changelog"", ""1.0a8 (2024-02-07)""]","[{""href"": ""https://github.com/simonw/datasette/issues/2225"", ""label"": ""#2225""}, {""href"": ""https://github.com/simonw/datasette/issues/1191"", ""label"": ""#1191""}, {""href"": ""https://github.com/simonw/datasette/issues/2240"", ""label"": ""#2240""}, {""href"": ""https://github.com/simonw/datasette/issues/2218"", ""label"": ""#2218""}]" changelog:id136,changelog,id136,0.20 (2018-04-20),"Mostly new work on the Plugins mechanism: plugins can now bundle static assets and custom templates, and datasette publish has a new --install=name-of-plugin option. Add col-X classes to HTML table on custom query page Fixed out-dated template in documentation Plugins can now bundle custom templates, #224 Added /-/metadata /-/plugins /-/inspect, #225 Documentation for --install option, refs #223 Datasette publish/package --install option, #223 Fix for plugins in Python 3.5, #222 New plugin hooks: extra_css_urls() and extra_js_urls(), #214 /-/static-plugins/PLUGIN_NAME/ now serves static/ from plugins <th> now gets class=""col-X"" - plus added col-X documentation Use to_css_class for table cell column classes This ensures that columns with spaces in the name will still generate usable CSS class names. Refs #209 Add column name classes to <td>s, make PK bold [Russ Garrett] Don't duplicate simple primary keys in the link column [Russ Garrett] When there's a simple (single-column) primary key, it looks weird to duplicate it in the link column. This change removes the second PK column and treats the link column as if it were the PK column from a header/sorting perspective. Correct escaping for HTML display of row links [Russ Garrett] Longer time limit for test_paginate_compound_keys It was failing intermittently in Travis - see #209 Use application/octet-stream for downloadable databases Updated PyPI classifiers Updated PyPI link to pypi.org","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/224"", ""label"": ""#224""}, {""href"": ""https://github.com/simonw/datasette/issues/225"", ""label"": ""#225""}, {""href"": ""https://github.com/simonw/datasette/issues/223"", ""label"": ""#223""}, {""href"": ""https://github.com/simonw/datasette/issues/223"", ""label"": ""#223""}, {""href"": ""https://github.com/simonw/datasette/issues/222"", ""label"": ""#222""}, {""href"": ""https://github.com/simonw/datasette/issues/214"", ""label"": ""#214""}, {""href"": ""https://github.com/simonw/datasette/issues/209"", ""label"": ""#209""}, {""href"": ""https://github.com/simonw/datasette/issues/209"", ""label"": ""#209""}]" changelog:permissions-fix-for-the-upsert-api,changelog,permissions-fix-for-the-upsert-api,Permissions fix for the upsert API,"The /database/table/-/upsert API had a minor permissions bug, only affecting Datasette instances that had configured the insert-row and update-row permissions to apply to a specific table rather than the database or instance as a whole. Full details in issue #2262 . To avoid similar mistakes in the future the datasette.permission_allowed() method now specifies default= as a keyword-only argument.","[""Changelog"", ""1.0a9 (2024-02-16)""]","[{""href"": ""https://github.com/simonw/datasette/issues/2262"", ""label"": ""#2262""}]" changelog:other-changes,changelog,other-changes,Other changes,"The new DATASETTE_TRACE_PLUGINS=1 environment variable turns on detailed trace output for every executed plugin hook, useful for debugging and understanding how the plugin system works at a low level. ( #2274 ) Datasette on Python 3.9 or above marks its non-cryptographic uses of the MD5 hash function as usedforsecurity=False , for compatibility with FIPS systems. ( #2270 ) SQL relating to Datasette's internal database now executes inside a transaction, avoiding a potential database locked error. ( #2273 ) The /-/threads debug page now identifies the database in the name associated with each dedicated write thread. ( #2265 ) The /db/-/create API now fires a insert-rows event if rows were inserted after the table was created. ( #2260 )","[""Changelog"", ""1.0a9 (2024-02-16)""]","[{""href"": ""https://github.com/simonw/datasette/issues/2274"", ""label"": ""#2274""}, {""href"": ""https://github.com/simonw/datasette/issues/2270"", ""label"": ""#2270""}, {""href"": ""https://github.com/simonw/datasette/issues/2273"", ""label"": ""#2273""}, {""href"": ""https://github.com/simonw/datasette/issues/2265"", ""label"": ""#2265""}, {""href"": ""https://github.com/simonw/datasette/issues/2260"", ""label"": ""#2260""}]" changelog:permission-checks-now-consider-opinions-from-every-plugin,changelog,permission-checks-now-consider-opinions-from-every-plugin,Permission checks now consider opinions from every plugin,"The datasette.permission_allowed() method previously consulted every plugin that implemented the permission_allowed() plugin hook and obeyed the opinion of the last plugin to return a value. ( #2275 ) Datasette now consults every plugin and checks to see if any of them returned False (the veto rule), and if none of them did, it then checks to see if any of them returned True . This is explained at length in the new documentation covering How permissions are resolved .","[""Changelog"", ""1.0a9 (2024-02-16)""]","[{""href"": ""https://github.com/simonw/datasette/issues/2275"", ""label"": ""#2275""}]" changelog:v1-0-a10,changelog,v1-0-a10,1.0a10 (2024-02-17),"The only changes in this alpha correspond to the way Datasette handles database transactions. ( #2277 ) The database.execute_write_fn() method has a new transaction=True parameter. This defaults to True which means all functions executed using this method are now automatically wrapped in a transaction - previously the functions needed to roll transaction handling on their own, and many did not. Pass transaction=False to execute_write_fn() if you want to manually handle transactions in your function. Several internal Datasette features, including parts of the JSON write API , had been failing to wrap their operations in a transaction. This has been fixed by the new transaction=True default.","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/2277"", ""label"": ""#2277""}]" changelog:v1-0-a11,changelog,v1-0-a11,1.0a11 (2024-02-19),"The ""replace"": true argument to the /db/table/-/insert API now requires the actor to have the update-row permission. ( #2279 ) Fixed some UI bugs in the interactive permissions debugging tool. ( #2278 ) The column action menu now aligns better with the cog icon, and positions itself taking into account the width of the browser window. ( #2263 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/2279"", ""label"": ""#2279""}, {""href"": ""https://github.com/simonw/datasette/issues/2278"", ""label"": ""#2278""}, {""href"": ""https://github.com/simonw/datasette/issues/2263"", ""label"": ""#2263""}]" changelog:v1-0-a12,changelog,v1-0-a12,1.0a12 (2024-02-29),"New query_actions() plugin hook, similar to table_actions() and database_actions() . Can be used to add a menu of actions to the canned query or arbitrary SQL query page. ( #2283 ) New design for the button that opens the query, table and database actions menu. ( #2281 ) ""does not contain"" table filter for finding rows that do not contain a string. ( #2287 ) Fixed a bug in the makeColumnActions(columnDetails) JavaScript plugin mechanism where the column action menu was not fully reset in between each interaction. ( #2289 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/2283"", ""label"": ""#2283""}, {""href"": ""https://github.com/simonw/datasette/issues/2281"", ""label"": ""#2281""}, {""href"": ""https://github.com/simonw/datasette/issues/2287"", ""label"": ""#2287""}, {""href"": ""https://github.com/simonw/datasette/issues/2289"", ""label"": ""#2289""}]" changelog:v1-0-a13,changelog,v1-0-a13,1.0a13 (2024-03-12),"Each of the key concepts in Datasette now has an actions menu , which plugins can use to add additional functionality targeting that entity. Plugin hook: view_actions() for actions that can be applied to a SQL view. ( #2297 ) Plugin hook: homepage_actions() for actions that apply to the instance homepage. ( #2298 ) Plugin hook: row_actions() for actions that apply to the row page. ( #2299 ) Action menu items for all of the *_actions() plugin hooks can now return an optional ""description"" key, which will be displayed in the menu below the action label. ( #2294 ) Plugin hooks documentation page is now organized with additional headings. ( #2300 ) Improved the display of action buttons on pages that also display metadata. ( #2286 ) The header and footer of the page now uses a subtle gradient effect, and options in the navigation menu are better visually defined. ( #2302 ) Table names that start with an underscore now default to hidden. ( #2104 ) pragma_table_list has been added to the allow-list of SQLite pragma functions supported by Datasette. select * from pragma_table_list() is no longer blocked. ( #2104 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/2297"", ""label"": ""#2297""}, {""href"": ""https://github.com/simonw/datasette/issues/2298"", ""label"": ""#2298""}, {""href"": ""https://github.com/simonw/datasette/issues/2299"", ""label"": ""#2299""}, {""href"": ""https://github.com/simonw/datasette/issues/2294"", ""label"": ""#2294""}, {""href"": ""https://github.com/simonw/datasette/issues/2300"", ""label"": ""#2300""}, {""href"": ""https://github.com/simonw/datasette/issues/2286"", ""label"": ""#2286""}, {""href"": ""https://github.com/simonw/datasette/issues/2302"", ""label"": ""#2302""}, {""href"": ""https://github.com/simonw/datasette/issues/2104"", ""label"": ""#2104""}, {""href"": ""https://github.com/simonw/datasette/issues/2104#issuecomment-1982352475"", ""label"": ""#2104""}]" changelog:id123,changelog,id123,0.21 (2018-05-05),"New JSON _shape= options, the ability to set table _size= and a mechanism for searching within specific columns. Default tests to using a longer timelimit Every now and then a test will fail in Travis CI on Python 3.5 because it hit the default 20ms SQL time limit. Test fixtures now default to a 200ms time limit, and we only use the 20ms time limit for the specific test that tests query interruption. This should make our tests on Python 3.5 in Travis much more stable. Support _search_COLUMN=text searches, closes #237 Show version on /-/plugins page, closes #248 ?_size=max option, closes #249 Added /-/versions and /-/versions.json , closes #244 Sample output: { ""python"": { ""version"": ""3.6.3"", ""full"": ""3.6.3 (default, Oct 4 2017, 06:09:38) \n[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.37)]"" }, ""datasette"": { ""version"": ""0.20"" }, ""sqlite"": { ""version"": ""3.23.1"", ""extensions"": { ""json1"": null, ""spatialite"": ""4.3.0a"" } } } Renamed ?_sql_time_limit_ms= to ?_timelimit , closes #242 New ?_shape=array option + tweaks to _shape , closes #245 Default is now ?_shape=arrays (renamed from lists ) New ?_shape=array returns an array of objects as the root object Changed ?_shape=object to return the object as the root Updated docs FTS tables now detected by inspect() , closes #240 New ?_size=XXX query string parameter for table view, closes #229 Also added documentation for all of the _special arguments. Plus deleted some duplicate logic implementing _group_count . If max_returned_rows==page_size , increment max_returned_rows - fixes #230 New hidden: True option for table metadata, closes #239 Hide idx_* tables if spatialite detected, closes #228 Added class=rows-and-columns to custom query results table Added CSS class rows-and-columns to main table label_column option in metadata.json - closes #234","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/237"", ""label"": ""#237""}, {""href"": ""https://github.com/simonw/datasette/issues/248"", ""label"": ""#248""}, {""href"": ""https://github.com/simonw/datasette/issues/249"", ""label"": ""#249""}, {""href"": ""https://github.com/simonw/datasette/issues/244"", ""label"": ""#244""}, {""href"": ""https://github.com/simonw/datasette/issues/242"", ""label"": ""#242""}, {""href"": ""https://github.com/simonw/datasette/issues/245"", ""label"": ""#245""}, {""href"": ""https://github.com/simonw/datasette/issues/240"", ""label"": ""#240""}, {""href"": ""https://github.com/simonw/datasette/issues/229"", ""label"": ""#229""}, {""href"": ""https://github.com/simonw/datasette/issues/230"", ""label"": ""#230""}, {""href"": ""https://github.com/simonw/datasette/issues/239"", ""label"": ""#239""}, {""href"": ""https://github.com/simonw/datasette/issues/228"", ""label"": ""#228""}, {""href"": ""https://github.com/simonw/datasette/issues/234"", ""label"": ""#234""}]" changelog:id33,changelog,id33,0.55 (2021-02-18),"Support for cross-database SQL queries and built-in support for serving via HTTPS. The new --crossdb command-line option causes Datasette to attach up to ten database files to the same /_memory database connection. This enables cross-database SQL queries, including the ability to use joins and unions to combine data from tables that exist in different database files. See Cross-database queries for details. ( #283 ) --ssl-keyfile and --ssl-certfile options can be used to specify a TLS certificate, allowing Datasette to serve traffic over https:// without needing to run it behind a separate proxy. ( #1221 ) The /:memory: page has been renamed (and redirected) to /_memory for consistency with the new /_internal database introduced in Datasette 0.54. ( #1205 ) Added plugin testing documentation on Using pdb for errors thrown inside Datasette . ( #1207 ) The official Datasette Docker image now uses Python 3.7.10, applying the latest security fix for that Python version. ( #1235 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/283"", ""label"": ""#283""}, {""href"": ""https://github.com/simonw/datasette/issues/1221"", ""label"": ""#1221""}, {""href"": ""https://github.com/simonw/datasette/issues/1205"", ""label"": ""#1205""}, {""href"": ""https://github.com/simonw/datasette/issues/1207"", ""label"": ""#1207""}, {""href"": ""https://hub.docker.com/r/datasetteproject/datasette"", ""label"": ""official Datasette Docker image""}, {""href"": ""https://www.python.org/downloads/release/python-3710/"", ""label"": ""the latest security fix""}, {""href"": ""https://github.com/simonw/datasette/issues/1235"", ""label"": ""#1235""}]" changelog:id107,changelog,id107,0.23.1 (2018-06-21),"Minor bugfix release. Correctly display empty strings in HTML table, closes #314 Allow ""."" in database filenames, closes #302 404s ending in slash redirect to remove that slash, closes #309 Fixed incorrect display of compound primary keys with foreign key references. Closes #319 Docs + example of canned SQL query using || concatenation. Closes #321 Correctly display facets with value of 0 - closes #318 Default 'expand labels' to checked in CSV advanced export","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/314"", ""label"": ""#314""}, {""href"": ""https://github.com/simonw/datasette/issues/302"", ""label"": ""#302""}, {""href"": ""https://github.com/simonw/datasette/issues/309"", ""label"": ""#309""}, {""href"": ""https://github.com/simonw/datasette/issues/319"", ""label"": ""#319""}, {""href"": ""https://github.com/simonw/datasette/issues/321"", ""label"": ""#321""}, {""href"": ""https://github.com/simonw/datasette/issues/318"", ""label"": ""#318""}]" changelog:id103,changelog,id103,0.23.2 (2018-07-07),"Minor bugfix and documentation release. CSV export now respects --cors , fixes #326 Installation instructions , including docker image - closes #328 Fix for row pages for tables with / in, closes #325","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/326"", ""label"": ""#326""}, {""href"": ""https://github.com/simonw/datasette/issues/328"", ""label"": ""#328""}, {""href"": ""https://github.com/simonw/datasette/issues/325"", ""label"": ""#325""}]" changelog:id94,changelog,id94,0.24 (2018-07-23),"A number of small new features: datasette publish heroku now supports --extra-options , fixes #334 Custom error message if SpatiaLite is needed for specified database, closes #331 New config option: truncate_cells_html for truncating long cell values in HTML view - closes #330 Documentation for datasette publish and datasette package , closes #337 Fixed compatibility with Python 3.7 datasette publish heroku now supports app names via the -n option, which can also be used to overwrite an existing application [Russ Garrett] Title and description metadata can now be set for canned SQL queries , closes #342 New force_https_on config option, fixes https:// API URLs when deploying to Zeit Now - closes #333 ?_json_infinity=1 query string argument for handling Infinity/-Infinity values in JSON, closes #332 URLs displayed in the results of custom SQL queries are now URLified, closes #298","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/334"", ""label"": ""#334""}, {""href"": ""https://github.com/simonw/datasette/issues/331"", ""label"": ""#331""}, {""href"": ""https://github.com/simonw/datasette/issues/330"", ""label"": ""#330""}, {""href"": ""https://github.com/simonw/datasette/issues/337"", ""label"": ""#337""}, {""href"": ""https://github.com/simonw/datasette/issues/342"", ""label"": ""#342""}, {""href"": ""https://github.com/simonw/datasette/issues/333"", ""label"": ""#333""}, {""href"": ""https://github.com/simonw/datasette/issues/332"", ""label"": ""#332""}, {""href"": ""https://github.com/simonw/datasette/issues/298"", ""label"": ""#298""}]" changelog:id93,changelog,id93,0.25 (2018-09-19),"New plugin hooks, improved database view support and an easier way to use more recent versions of SQLite. New publish_subcommand plugin hook. A plugin can now add additional datasette publish publishers in addition to the default now and heroku , both of which have been refactored into default plugins. publish_subcommand documentation . Closes #349 New render_cell plugin hook. Plugins can now customize how values are displayed in the HTML tables produced by Datasette's browsable interface. datasette-json-html and datasette-render-images are two new plugins that use this hook. render_cell documentation . Closes #352 New extra_body_script plugin hook, enabling plugins to provide additional JavaScript that should be added to the page footer. extra_body_script documentation . extra_css_urls and extra_js_urls hooks now take additional optional parameters, allowing them to be more selective about which pages they apply to. Documentation . You can now use the sortable_columns metadata setting to explicitly enable sort-by-column in the interface for database views, as well as for specific tables. The new fts_table and fts_pk metadata settings can now be used to explicitly configure full-text search for a table or a view , even if that table is not directly coupled to the SQLite FTS feature in the database schema itself. Datasette will now use pysqlite3 in place of the standard library sqlite3 module if it has been installed in the current environment. This makes it much easier to run Datasette against a more recent version of SQLite, including the just-released SQLite 3.25.0 which adds window function support. More details on how to use this in #360 New mechanism that allows plugin configuration options to be set using metadata.json .","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/349"", ""label"": ""#349""}, {""href"": ""https://github.com/simonw/datasette-json-html"", ""label"": ""datasette-json-html""}, {""href"": ""https://github.com/simonw/datasette-render-images"", ""label"": ""datasette-render-images""}, {""href"": ""https://github.com/simonw/datasette/issues/352"", ""label"": ""#352""}, {""href"": ""https://github.com/coleifer/pysqlite3"", ""label"": ""pysqlite3""}, {""href"": ""https://www.sqlite.org/releaselog/3_25_0.html"", ""label"": ""SQLite 3.25.0""}, {""href"": ""https://github.com/simonw/datasette/issues/360"", ""label"": ""#360""}]" changelog:v0-28-faceting,changelog,v0-28-faceting,"Faceting improvements, and faceting plugins","Datasette Facets provide an intuitive way to quickly summarize and interact with data. Previously the only supported faceting technique was column faceting, but 0.28 introduces two powerful new capabilities: facet-by-JSON-array and the ability to define further facet types using plugins. Facet by array ( #359 ) is only available if your SQLite installation provides the json1 extension. Datasette will automatically detect columns that contain JSON arrays of values and offer a faceting interface against those columns - useful for modelling things like tags without needing to break them out into a new table. See Facet by JSON array for more. The new register_facet_classes() plugin hook ( #445 ) can be used to register additional custom facet classes. Each facet class should provide two methods: suggest() which suggests facet selections that might be appropriate for a provided SQL query, and facet_results() which executes a facet operation and returns results. Datasette's own faceting implementations have been refactored to use the same API as these plugins.","[""Changelog"", ""0.28 (2019-05-19)""]","[{""href"": ""https://github.com/simonw/datasette/issues/359"", ""label"": ""#359""}, {""href"": ""https://github.com/simonw/datasette/pull/445"", ""label"": ""#445""}]" changelog:id92,changelog,id92,0.25.1 (2018-11-04),"Documentation improvements plus a fix for publishing to Zeit Now. datasette publish now now uses Zeit's v1 platform, to work around the new 100MB image limit. Thanks, @slygent - closes #366 .","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/366"", ""label"": ""#366""}]" changelog:id90,changelog,id90,0.26 (2019-01-02),"datasette serve --reload now restarts Datasette if a database file changes on disk. datasette publish now now takes an optional --alias mysite.now.sh argument. This will attempt to set an alias after the deploy completes. Fixed a bug where the advanced CSV export form failed to include the currently selected filters ( #393 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/393"", ""label"": ""#393""}]" changelog:id66,changelog,id66,0.39 (2020-03-24),"New base_url configuration setting for serving up the correct links while running Datasette under a different URL prefix. ( #394 ) New metadata settings ""sort"" and ""sort_desc"" for setting the default sort order for a table. See Setting a default sort order . ( #702 ) Sort direction arrow now displays by default on the primary key. This means you only have to click once (not twice) to sort in reverse order. ( #677 ) New await Request(scope, receive).post_vars() method for accessing POST form variables. ( #700 ) Plugin hooks documentation now links to example uses of each plugin. ( #709 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/394"", ""label"": ""#394""}, {""href"": ""https://github.com/simonw/datasette/issues/702"", ""label"": ""#702""}, {""href"": ""https://github.com/simonw/datasette/issues/677"", ""label"": ""#677""}, {""href"": ""https://github.com/simonw/datasette/issues/700"", ""label"": ""#700""}, {""href"": ""https://github.com/simonw/datasette/issues/709"", ""label"": ""#709""}]" changelog:id89,changelog,id89,0.26.1 (2019-01-10),"/-/versions now includes SQLite compile_options ( #396 ) datasetteproject/datasette Docker image now uses SQLite 3.26.0 ( #397 ) Cleaned up some deprecation warnings under Python 3.7","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/396"", ""label"": ""#396""}, {""href"": ""https://hub.docker.com/r/datasetteproject/datasette"", ""label"": ""datasetteproject/datasette""}, {""href"": ""https://github.com/simonw/datasette/issues/397"", ""label"": ""#397""}]" changelog:the-internal-database,changelog,the-internal-database,The _internal database,"As part of ongoing work to help Datasette handle much larger numbers of connected databases and tables (see Datasette Library ) Datasette now maintains an in-memory SQLite database with details of all of the attached databases, tables, columns, indexes and foreign keys. ( #1150 ) This will support future improvements such as a searchable, paginated homepage of all available tables. You can explore an example of this database by signing in as root to the latest.datasette.io demo instance and then navigating to latest.datasette.io/_internal . Plugins can use these tables to introspect attached data in an efficient way. Plugin authors should note that this is not yet considered a stable interface, so any plugins that use this may need to make changes prior to Datasette 1.0 if the _internal table schemas change.","[""Changelog"", ""0.54 (2021-01-25)""]","[{""href"": ""https://github.com/simonw/datasette/issues/417"", ""label"": ""Datasette Library""}, {""href"": ""https://github.com/simonw/datasette/issues/1150"", ""label"": ""#1150""}, {""href"": ""https://latest.datasette.io/login-as-root"", ""label"": ""signing in as root""}, {""href"": ""https://latest.datasette.io/_internal"", ""label"": ""latest.datasette.io/_internal""}]" changelog:small-changes,changelog,small-changes,Small changes,"Databases published using datasette publish now open in Immutable mode . ( #469 ) ?col__date= now works for columns containing spaces Automatic label detection (for deciding which column to show when linking to a foreign key) has been improved. ( #485 ) Fixed bug where pagination broke when combined with an expanded foreign key. ( #489 ) Contributors can now run pip install -e .[docs] to get all of the dependencies needed to build the documentation, including cd docs && make livehtml support. Datasette's dependencies are now all specified using the ~= match operator. ( #532 ) white-space: pre-wrap now used for table creation SQL. ( #505 ) Full list of commits between 0.28 and 0.29.","[""Changelog"", ""0.29 (2019-07-07)""]","[{""href"": ""https://github.com/simonw/datasette/issues/469"", ""label"": ""#469""}, {""href"": ""https://github.com/simonw/datasette/issues/485"", ""label"": ""#485""}, {""href"": ""https://github.com/simonw/datasette/issues/489"", ""label"": ""#489""}, {""href"": ""https://github.com/simonw/datasette/issues/532"", ""label"": ""#532""}, {""href"": ""https://github.com/simonw/datasette/issues/505"", ""label"": ""#505""}, {""href"": ""https://github.com/simonw/datasette/compare/0.28...0.29"", ""label"": ""Full list of commits""}]" changelog:plugins-and-internals,changelog,plugins-and-internals,Plugins and internals,"New plugin hook: filters_from_request(request, database, table, datasette) , which runs on the table page and can be used to support new custom query string parameters that modify the SQL query. ( #473 ) Added two additional methods for writing to the database: await db.execute_write_script(sql, block=True) and await db.execute_write_many(sql, params_seq, block=True) . ( #1570 ) The db.execute_write() internal method now defaults to blocking until the write operation has completed. Previously it defaulted to queuing the write and then continuing to run code while the write was in the queue. ( #1579 ) Database write connections now execute the prepare_connection(conn, database, datasette) plugin hook. ( #1564 ) The Datasette() constructor no longer requires the files= argument, and is now documented at Datasette class . ( #1563 ) The tracing feature now traces write queries, not just read queries. ( #1568 ) The query string variables exposed by request.args will now include blank strings for arguments such as foo in ?foo=&bar=1 rather than ignoring those parameters entirely. ( #1551 )","[""Changelog"", ""0.60 (2022-01-13)""]","[{""href"": ""https://github.com/simonw/datasette/issues/473"", ""label"": ""#473""}, {""href"": ""https://github.com/simonw/datasette/issues/1570"", ""label"": ""#1570""}, {""href"": ""https://github.com/simonw/datasette/issues/1579"", ""label"": ""#1579""}, {""href"": ""https://github.com/simonw/datasette/issues/1564"", ""label"": ""#1564""}, {""href"": ""https://github.com/simonw/datasette/issues/1563"", ""label"": ""#1563""}, {""href"": ""https://github.com/simonw/datasette/issues/1568"", ""label"": ""#1568""}, {""href"": ""https://github.com/simonw/datasette/issues/1551"", ""label"": ""#1551""}]" changelog:facet-by-date,changelog,facet-by-date,Facet by date,"If a column contains datetime values, Datasette can now facet that column by date. ( #481 )","[""Changelog"", ""0.29 (2019-07-07)""]","[{""href"": ""https://github.com/simonw/datasette/issues/481"", ""label"": ""#481""}]" changelog:id36,changelog,id36,Other changes,"Datasette can now open multiple database files with the same name, e.g. if you run datasette path/to/one.db path/to/other/one.db . ( #509 ) datasette publish cloudrun now sets force_https_urls for every deployment, fixing some incorrect http:// links. ( #1178 ) Fixed a bug in the example nginx configuration in Running Datasette behind a proxy . ( #1091 ) The Datasette Ecosystem documentation page has been reduced in size in favour of the datasette.io tools and plugins directories. ( #1182 ) The request object now provides a request.full_path property, which returns the path including any query string. ( #1184 ) Better error message for disallowed PRAGMA clauses in SQL queries. ( #1185 ) datasette publish heroku now deploys using python-3.8.7 . New plugin testing documentation on Testing outbound HTTP calls with pytest-httpx . ( #1198 ) All ?_* query string parameters passed to the table page are now persisted in hidden form fields, so parameters such as ?_size=10 will be correctly passed to the next page when query filters are changed. ( #1194 ) Fixed a bug loading a database file called test-database (1).sqlite . ( #1181 )","[""Changelog"", ""0.54 (2021-01-25)""]","[{""href"": ""https://github.com/simonw/datasette/issues/509"", ""label"": ""#509""}, {""href"": ""https://github.com/simonw/datasette/issues/1178"", ""label"": ""#1178""}, {""href"": ""https://github.com/simonw/datasette/issues/1091"", ""label"": ""#1091""}, {""href"": ""https://datasette.io/tools"", ""label"": ""tools""}, {""href"": ""https://datasette.io/plugins"", ""label"": ""plugins""}, {""href"": ""https://github.com/simonw/datasette/issues/1182"", ""label"": ""#1182""}, {""href"": ""https://github.com/simonw/datasette/issues/1184"", ""label"": ""#1184""}, {""href"": ""https://github.com/simonw/datasette/issues/1185"", ""label"": ""#1185""}, {""href"": ""https://github.com/simonw/datasette/issues/1198"", ""label"": ""#1198""}, {""href"": ""https://github.com/simonw/datasette/issues/1194"", ""label"": ""#1194""}, {""href"": ""https://github.com/simonw/datasette/issues/1181"", ""label"": ""#1181""}]" changelog:new-plugin-hook-asgi-wrapper,changelog,new-plugin-hook-asgi-wrapper,New plugin hook: asgi_wrapper,"The asgi_wrapper(datasette) plugin hook allows plugins to entirely wrap the Datasette ASGI application in their own ASGI middleware. ( #520 ) Two new plugins take advantage of this hook: datasette-auth-github adds a authentication layer: users will have to sign in using their GitHub account before they can view data or interact with Datasette. You can also use it to restrict access to specific GitHub users, or to members of specified GitHub organizations or teams . datasette-cors allows you to configure CORS headers for your Datasette instance. You can use this to enable JavaScript running on a whitelisted set of domains to make fetch() calls to the JSON API provided by your Datasette instance.","[""Changelog"", ""0.29 (2019-07-07)""]","[{""href"": ""https://github.com/simonw/datasette/issues/520"", ""label"": ""#520""}, {""href"": ""https://github.com/simonw/datasette-auth-github"", ""label"": ""datasette-auth-github""}, {""href"": ""https://help.github.com/en/articles/about-organizations"", ""label"": ""organizations""}, {""href"": ""https://help.github.com/en/articles/organizing-members-into-teams"", ""label"": ""teams""}, {""href"": ""https://github.com/simonw/datasette-cors"", ""label"": ""datasette-cors""}, {""href"": ""https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS"", ""label"": ""CORS headers""}]" changelog:id83,changelog,id83,0.29.1 (2019-07-11),"Fixed bug with static mounts using relative paths which could lead to traversal exploits ( #555 ) - thanks Abdussamet Kocak! Datasette can now be run as a module: python -m datasette ( #556 ) - thanks, Abdussamet Kocak!","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/555"", ""label"": ""#555""}, {""href"": ""https://github.com/simonw/datasette/issues/556"", ""label"": ""#556""}]" changelog:id81,changelog,id81,0.29.3 (2019-09-02),"Fixed implementation of CodeMirror on database page ( #560 ) Documentation typo fixes - thanks, Min ho Kim ( #561 ) Mechanism for detecting if a table has FTS enabled now works if the table name used alternative escaping mechanisms ( #570 ) - for compatibility with a recent change to sqlite-utils .","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/560"", ""label"": ""#560""}, {""href"": ""https://github.com/simonw/datasette/pull/561"", ""label"": ""#561""}, {""href"": ""https://github.com/simonw/datasette/issues/570"", ""label"": ""#570""}, {""href"": ""https://github.com/simonw/sqlite-utils/pull/57"", ""label"": ""a recent change to sqlite-utils""}]" changelog:id70,changelog,id70,0.36 (2020-02-21),"The datasette object passed to plugins now has API documentation: Datasette class . ( #576 ) New methods on datasette : .add_database() and .remove_database() - documentation . ( #671 ) prepare_connection() plugin hook now takes optional datasette and database arguments - prepare_connection(conn, database, datasette) . ( #678 ) Added three new plugins and one new conversion tool to the The Datasette Ecosystem .","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/576"", ""label"": ""#576""}, {""href"": ""https://github.com/simonw/datasette/issues/671"", ""label"": ""#671""}, {""href"": ""https://github.com/simonw/datasette/issues/678"", ""label"": ""#678""}]" changelog:id80,changelog,id80,0.30 (2019-10-18),"Added /-/threads debugging page Allow EXPLAIN WITH... ( #583 ) Button to format SQL - thanks, Tobias Kunze ( #136 ) Sort databases on homepage by argument order - thanks, Tobias Kunze ( #585 ) Display metadata footer on custom SQL queries - thanks, Tobias Kunze ( #589 ) Use --platform=managed for publish cloudrun ( #587 ) Fixed bug returning non-ASCII characters in CSV ( #584 ) Fix for /foo v.s. /foo-bar bug ( #601 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/583"", ""label"": ""#583""}, {""href"": ""https://github.com/simonw/datasette/issues/136"", ""label"": ""#136""}, {""href"": ""https://github.com/simonw/datasette/issues/585"", ""label"": ""#585""}, {""href"": ""https://github.com/simonw/datasette/pull/589"", ""label"": ""#589""}, {""href"": ""https://github.com/simonw/datasette/issues/587"", ""label"": ""#587""}, {""href"": ""https://github.com/simonw/datasette/issues/584"", ""label"": ""#584""}, {""href"": ""https://github.com/simonw/datasette/issues/601"", ""label"": ""#601""}]" changelog:id79,changelog,id79,0.30.1 (2019-10-30),"Fixed bug where ?_where= parameter was not persisted in hidden form fields ( #604 ) Fixed bug with .JSON representation of row pages - thanks, Chris Shaw ( #603 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/604"", ""label"": ""#604""}, {""href"": ""https://github.com/simonw/datasette/issues/603"", ""label"": ""#603""}]" changelog:id78,changelog,id78,0.30.2 (2019-11-02),"/-/plugins page now uses distribution name e.g. datasette-cluster-map instead of the name of the underlying Python package ( datasette_cluster_map ) ( #606 ) Array faceting is now only suggested for columns that contain arrays of strings ( #562 ) Better documentation for the --host argument ( #574 ) Don't show None with a broken link for the label on a nullable foreign key ( #406 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/606"", ""label"": ""#606""}, {""href"": ""https://github.com/simonw/datasette/issues/562"", ""label"": ""#562""}, {""href"": ""https://github.com/simonw/datasette/issues/574"", ""label"": ""#574""}, {""href"": ""https://github.com/simonw/datasette/issues/406"", ""label"": ""#406""}]" changelog:new-features,changelog,new-features,New features,"If an error occurs while executing a user-provided SQL query, that query is now re-displayed in an editable form along with the error message. ( #619 ) New ?_col= and ?_nocol= parameters to show and hide columns in a table, plus an interface for hiding and showing columns in the column cog menu. ( #615 ) A new ?_facet_size= parameter for customizing the number of facet results returned on a table or view page. ( #1332 ) ?_facet_size=max sets that to the maximum, which defaults to 1,000 and is controlled by the the max_returned_rows setting. If facet results are truncated the … at the bottom of the facet list now links to this parameter. ( #1337 ) ?_nofacet=1 option to disable all facet calculations on a page, used as a performance optimization for CSV exports and ?_shape=array/object . ( #1349 , #263 ) ?_nocount=1 option to disable full query result counts. ( #1353 ) ?_trace=1 debugging option is now controlled by the new trace_debug setting, which is turned off by default. ( #1359 )","[""Changelog"", ""0.57 (2021-06-05)""]","[{""href"": ""https://github.com/simonw/datasette/issues/619"", ""label"": ""#619""}, {""href"": ""https://github.com/simonw/datasette/issues/615"", ""label"": ""#615""}, {""href"": ""https://github.com/simonw/datasette/issues/1332"", ""label"": ""#1332""}, {""href"": ""https://github.com/simonw/datasette/issues/1337"", ""label"": ""#1337""}, {""href"": ""https://github.com/simonw/datasette/issues/1349"", ""label"": ""#1349""}, {""href"": ""https://github.com/simonw/datasette/issues/263"", ""label"": ""#263""}, {""href"": ""https://github.com/simonw/datasette/issues/1353"", ""label"": ""#1353""}, {""href"": ""https://github.com/simonw/datasette/issues/1359"", ""label"": ""#1359""}]" changelog:id75,changelog,id75,0.31.2 (2019-11-13),"Fixed a bug where datasette publish heroku applications failed to start ( #633 ) Fix for datasette publish with just --source_url - thanks, Stanley Zheng ( #572 ) Deployments to Heroku now use Python 3.8.0 ( #632 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/633"", ""label"": ""#633""}, {""href"": ""https://github.com/simonw/datasette/issues/572"", ""label"": ""#572""}, {""href"": ""https://github.com/simonw/datasette/issues/632"", ""label"": ""#632""}]" changelog:id73,changelog,id73,0.33 (2019-12-22),"rowid is now included in dropdown menus for filtering tables ( #636 ) Columns are now only suggested for faceting if they have at least one value with more than one record ( #638 ) Queries with no results now display ""0 results"" ( #637 ) Improved documentation for the --static option ( #641 ) asyncio task information is now included on the /-/threads debug page Bumped Uvicorn dependency 0.11 You can now use --port 0 to listen on an available port New template_debug setting for debugging templates, e.g. https://latest.datasette.io/fixtures/roadside_attractions?_context=1 ( #654 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/636"", ""label"": ""#636""}, {""href"": ""https://github.com/simonw/datasette/issues/638"", ""label"": ""#638""}, {""href"": ""https://github.com/simonw/datasette/issues/637"", ""label"": ""#637""}, {""href"": ""https://github.com/simonw/datasette/issues/641"", ""label"": ""#641""}, {""href"": ""https://latest.datasette.io/fixtures/roadside_attractions?_context=1"", ""label"": ""https://latest.datasette.io/fixtures/roadside_attractions?_context=1""}, {""href"": ""https://github.com/simonw/datasette/issues/654"", ""label"": ""#654""}]" changelog:id64,changelog,id64,0.41 (2020-05-06),"You can now create custom pages within your Datasette instance using a custom template file. For example, adding a template file called templates/pages/about.html will result in a new page being served at /about on your instance. See the custom pages documentation for full details, including how to return custom HTTP headers, redirects and status codes. ( #648 ) Configuration directory mode ( #731 ) allows you to define a custom Datasette instance as a directory. So instead of running the following: datasette one.db two.db \ --metadata=metadata.json \ --template-dir=templates/ \ --plugins-dir=plugins \ --static css:css You can instead arrange your files in a single directory called my-project and run this: datasette my-project/ Also in this release: New NOT LIKE table filter: ?colname__notlike=expression . ( #750 ) Datasette now has a pattern portfolio at /-/patterns - e.g. https://latest.datasette.io/-/patterns . This is a page that shows every Datasette user interface component in one place, to aid core development and people building custom CSS themes. ( #151 ) SQLite PRAGMA functions such as pragma_table_info(tablename) are now allowed in Datasette SQL queries. ( #761 ) Datasette pages now consistently return a content-type of text/html; charset=utf-8"" . ( #752 ) Datasette now handles an ASGI raw_path value of None , which should allow compatibility with the Mangum adapter for running ASGI apps on AWS Lambda. Thanks, Colin Dellow. ( #719 ) Installation documentation now covers how to Using pipx . ( #756 ) Improved the documentation for Full-text search . ( #748 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/648"", ""label"": ""#648""}, {""href"": ""https://github.com/simonw/datasette/issues/731"", ""label"": ""#731""}, {""href"": ""https://github.com/simonw/datasette/issues/750"", ""label"": ""#750""}, {""href"": ""https://latest.datasette.io/-/patterns"", ""label"": ""https://latest.datasette.io/-/patterns""}, {""href"": ""https://github.com/simonw/datasette/issues/151"", ""label"": ""#151""}, {""href"": ""https://www.sqlite.org/pragma.html#pragfunc"", ""label"": ""PRAGMA functions""}, {""href"": ""https://github.com/simonw/datasette/issues/761"", ""label"": ""#761""}, {""href"": ""https://github.com/simonw/datasette/issues/752"", ""label"": ""#752""}, {""href"": ""https://github.com/erm/mangum"", ""label"": ""Mangum""}, {""href"": ""https://github.com/simonw/datasette/pull/719"", ""label"": ""#719""}, {""href"": ""https://github.com/simonw/datasette/issues/756"", ""label"": ""#756""}, {""href"": ""https://github.com/simonw/datasette/issues/748"", ""label"": ""#748""}]" changelog:id72,changelog,id72,0.34 (2020-01-29),"_search= queries are now correctly escaped using a new escape_fts() custom SQL function. This means you can now run searches for strings like park. without seeing errors. ( #651 ) Google Cloud Run is no longer in beta, so datasette publish cloudrun has been updated to work even if the user has not installed the gcloud beta components package. Thanks, Katie McLaughlin ( #660 ) datasette package now accepts a --port option for specifying which port the resulting Docker container should listen on. ( #661 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/651"", ""label"": ""#651""}, {""href"": ""https://cloud.google.com/run/"", ""label"": ""Google Cloud Run""}, {""href"": ""https://github.com/simonw/datasette/pull/660"", ""label"": ""#660""}, {""href"": ""https://github.com/simonw/datasette/issues/661"", ""label"": ""#661""}]" changelog:id69,changelog,id69,0.37 (2020-02-25),"Plugins now have a supported mechanism for writing to a database, using the new .execute_write() and .execute_write_fn() methods. Documentation . ( #682 ) Immutable databases that have had their rows counted using the inspect command now use the calculated count more effectively - thanks, Kevin Keogh. ( #666 ) --reload no longer restarts the server if a database file is modified, unless that database was opened immutable mode with -i . ( #494 ) New ?_searchmode=raw option turns off escaping for FTS queries in ?_search= allowing full use of SQLite's FTS5 query syntax . ( #676 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/682"", ""label"": ""#682""}, {""href"": ""https://github.com/simonw/datasette/pull/666"", ""label"": ""#666""}, {""href"": ""https://github.com/simonw/datasette/issues/494"", ""label"": ""#494""}, {""href"": ""https://www.sqlite.org/fts5.html#full_text_query_syntax"", ""label"": ""FTS5 query syntax""}, {""href"": ""https://github.com/simonw/datasette/issues/676"", ""label"": ""#676""}]" changelog:id63,changelog,id63,0.42 (2020-05-08),"A small release which provides improved internal methods for use in plugins, along with documentation. See #685 . Added documentation for db.execute() , see await db.execute(sql, ...) . Renamed db.execute_against_connection_in_thread() to db.execute_fn() and made it a documented method, see await db.execute_fn(fn) . New results.first() and results.single_value() methods, plus documentation for the Results class - see Results .","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/685"", ""label"": ""#685""}]" changelog:better-plugin-documentation,changelog,better-plugin-documentation,Better plugin documentation,"The plugin documentation has been re-arranged into four sections, including a brand new section on testing plugins. ( #687 ) Plugins introduces Datasette's plugin system and describes how to install and configure plugins. Writing plugins describes how to author plugins, from one-off single file plugins to packaged plugins that can be published to PyPI. It also describes how to start a plugin using the new datasette-plugin cookiecutter template. Plugin hooks is a full list of detailed documentation for every Datasette plugin hook. Testing plugins describes how to write tests for Datasette plugins, using pytest and HTTPX .","[""Changelog"", ""0.45 (2020-07-01)""]","[{""href"": ""https://github.com/simonw/datasette/issues/687"", ""label"": ""#687""}, {""href"": ""https://github.com/simonw/datasette-plugin"", ""label"": ""datasette-plugin""}, {""href"": ""https://docs.pytest.org/"", ""label"": ""pytest""}, {""href"": ""https://www.python-httpx.org/"", ""label"": ""HTTPX""}]" changelog:id68,changelog,id68,0.37.1 (2020-03-02),"Don't attempt to count table rows to display on the index page for databases > 100MB. ( #688 ) Print exceptions if they occur in the write thread rather than silently swallowing them. Handle the possibility of scope[""path""] being a string rather than bytes Better documentation for the extra_template_vars(template, database, table, columns, view_name, request, datasette) plugin hook.","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/688"", ""label"": ""#688""}]" changelog:id212,changelog,id212,0.8 (2017-11-13),"V0.8 - added PyPI metadata, ready to ship. Implemented offset/limit pagination for views ( #70 ). Improved pagination. ( #78 ) Limit on max rows returned, controlled by --max_returned_rows option. ( #69 ) If someone executes 'select * from table' against a table with a million rows in it, we could run into problems: just serializing that much data as JSON is likely to lock up the server. Solution: we now have a hard limit on the maximum number of rows that can be returned by a query. If that limit is exceeded, the server will return a ""truncated"": true field in the JSON. This limit can be optionally controlled by the new --max_returned_rows option. Setting that option to 0 disables the limit entirely.","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/70"", ""label"": ""#70""}, {""href"": ""https://github.com/simonw/datasette/issues/78"", ""label"": ""#78""}, {""href"": ""https://github.com/simonw/datasette/issues/69"", ""label"": ""#69""}]" changelog:id61,changelog,id61,Smaller changes,"New internals documentation for Request object and Response class . ( #706 ) request.url now respects the force_https_urls config setting. closes ( #781 ) request.args.getlist() returns [] if missing. Removed request.raw_args entirely. ( #774 ) New datasette.get_database() method. Added _ prefix to many private, undocumented methods of the Datasette class. ( #576 ) Removed the db.get_outbound_foreign_keys() method which duplicated the behaviour of db.foreign_keys_for_table() . New await datasette.permission_allowed() method. /-/actor debugging endpoint for viewing the currently authenticated actor. New request.cookies property. /-/plugins endpoint now shows a list of hooks implemented by each plugin, e.g. https://latest.datasette.io/-/plugins?all=1 request.post_vars() method no longer discards empty values. New ""params"" canned query key for explicitly setting named parameters, see Canned query parameters . ( #797 ) request.args is now a MultiParams object. Fixed a bug with the datasette plugins command. ( #802 ) Nicer pattern for using make_app_client() in tests. ( #395 ) New request.actor property. Fixed broken CSS on nested 404 pages. ( #777 ) New request.url_vars property. ( #822 ) Fixed a bug with the python tests/fixtures.py command for outputting Datasette's testing fixtures database and plugins. ( #804 ) datasette publish heroku now deploys using Python 3.8.3. Added a warning that the register_facet_classes() hook is unstable and may change in the future. ( #830 ) The {""$env"": ""ENVIRONMENT_VARIBALE""} mechanism (see Secret configuration values ) now works with variables inside nested lists. ( #837 )","[""Changelog"", ""0.44 (2020-06-11)""]","[{""href"": ""https://github.com/simonw/datasette/issues/706"", ""label"": ""#706""}, {""href"": ""https://github.com/simonw/datasette/issues/781"", ""label"": ""#781""}, {""href"": ""https://github.com/simonw/datasette/issues/774"", ""label"": ""#774""}, {""href"": ""https://github.com/simonw/datasette/issues/576"", ""label"": ""#576""}, {""href"": ""https://latest.datasette.io/-/plugins?all=1"", ""label"": ""https://latest.datasette.io/-/plugins?all=1""}, {""href"": ""https://github.com/simonw/datasette/issues/797"", ""label"": ""#797""}, {""href"": ""https://github.com/simonw/datasette/issues/802"", ""label"": ""#802""}, {""href"": ""https://github.com/simonw/datasette/issues/395"", ""label"": ""#395""}, {""href"": ""https://github.com/simonw/datasette/issues/777"", ""label"": ""#777""}, {""href"": ""https://github.com/simonw/datasette/issues/822"", ""label"": ""#822""}, {""href"": ""https://github.com/simonw/datasette/issues/804"", ""label"": ""#804""}, {""href"": ""https://github.com/simonw/datasette/issues/830"", ""label"": ""#830""}, {""href"": ""https://github.com/simonw/datasette/issues/837"", ""label"": ""#837""}]" changelog:id65,changelog,id65,0.40 (2020-04-21),"Datasette Metadata can now be provided as a YAML file as an optional alternative to JSON. ( #713 ) Removed support for datasette publish now , which used the the now-retired Zeit Now v1 hosting platform. A new plugin, datasette-publish-now , can be installed to publish data to Zeit ( now Vercel ) Now v2. ( #710 ) Fixed a bug where the extra_template_vars(request, view_name) plugin hook was not receiving the correct view_name . ( #716 ) Variables added to the template context by the extra_template_vars() plugin hook are now shown in the ?_context=1 debugging mode (see template_debug ). ( #693 ) Fixed a bug where the ""templates considered"" HTML comment was no longer being displayed. ( #689 ) Fixed a datasette publish bug where --plugin-secret would over-ride plugin configuration in the provided metadata.json file. ( #724 ) Added a new CSS class for customizing the canned query page. ( #727 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/713"", ""label"": ""#713""}, {""href"": ""https://github.com/simonw/datasette-publish-now"", ""label"": ""datasette-publish-now""}, {""href"": ""https://vercel.com/blog/zeit-is-now-vercel"", ""label"": ""now Vercel""}, {""href"": ""https://github.com/simonw/datasette/issues/710"", ""label"": ""#710""}, {""href"": ""https://github.com/simonw/datasette/issues/716"", ""label"": ""#716""}, {""href"": ""https://github.com/simonw/datasette/issues/693"", ""label"": ""#693""}, {""href"": ""https://github.com/simonw/datasette/issues/689"", ""label"": ""#689""}, {""href"": ""https://github.com/simonw/datasette/issues/724"", ""label"": ""#724""}, {""href"": ""https://github.com/simonw/datasette/issues/727"", ""label"": ""#727""}]" sql_queries:sql-parameters,sql_queries,sql-parameters,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.","[""Running SQL queries""]","[{""href"": ""https://github.com/simonw/datasette/issues/761"", ""label"": ""of exceptions""}]" changelog:v1-0-a3,changelog,v1-0-a3,1.0a3 (2023-08-09),"This alpha release previews the updated design for Datasette's default JSON API. ( #782 ) The new default JSON representation for both table pages ( /dbname/table.json ) and arbitrary SQL queries ( /dbname.json?sql=... ) is now shaped like this: { ""ok"": true, ""rows"": [ { ""id"": 3, ""name"": ""Detroit"" }, { ""id"": 2, ""name"": ""Los Angeles"" }, { ""id"": 4, ""name"": ""Memnonia"" }, { ""id"": 1, ""name"": ""San Francisco"" } ], ""truncated"": false } Tables will include an additional ""next"" key for pagination, which can be passed to ?_next= to fetch the next page of results. The various ?_shape= options continue to work as before - see Different shapes for details. A new ?_extra= mechanism is available for tables, but has not yet been stabilized or documented. Details on that are available in #262 .","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/782"", ""label"": ""#782""}, {""href"": ""https://github.com/simonw/datasette/issues/262"", ""label"": ""#262""}]" changelog:permissions,changelog,permissions,Permissions,"Datasette also now has a built-in concept of Permissions . The permissions system answers the following question: Is this actor allowed to perform this action , optionally against this particular resource ? You can use the new ""allow"" block syntax in metadata.json (or metadata.yaml ) to set required permissions at the instance, database, table or canned query level. For example, to restrict access to the fixtures.db database to the ""root"" user: { ""databases"": { ""fixtures"": { ""allow"": { ""id"" ""root"" } } } } See Defining permissions with ""allow"" blocks for more details. Plugins can implement their own custom permission checks using the new permission_allowed(datasette, actor, action, resource) hook. A new debug page at /-/permissions shows recent permission checks, to help administrators and plugin authors understand exactly what checks are being performed. This tool defaults to only being available to the root user, but can be exposed to other users by plugins that respond to the permissions-debug permission. ( #788 )","[""Changelog"", ""0.44 (2020-06-11)""]","[{""href"": ""https://github.com/simonw/datasette/issues/788"", ""label"": ""#788""}]" changelog:flash-messages,changelog,flash-messages,Flash messages,"Writable canned queries needed a mechanism to let the user know that the query has been successfully executed. The new flash messaging system ( #790 ) allows messages to persist in signed cookies which are then displayed to the user on the next page that they visit. Plugins can use this mechanism to display their own messages, see .add_message(request, message, type=datasette.INFO) for details. You can try out the new messages using the /-/messages debug tool, for example at https://latest.datasette.io/-/messages","[""Changelog"", ""0.44 (2020-06-11)""]","[{""href"": ""https://github.com/simonw/datasette/issues/790"", ""label"": ""#790""}, {""href"": ""https://latest.datasette.io/-/messages"", ""label"": ""https://latest.datasette.io/-/messages""}]" changelog:csrf-protection,changelog,csrf-protection,CSRF protection,"Since writable canned queries are built using POST forms, Datasette now ships with CSRF protection ( #798 ). This applies automatically to any POST request, which means plugins need to include a csrftoken in any POST forms that they render. They can do that like so: <input type=""hidden"" name=""csrftoken"" value=""{{ csrftoken() }}"">","[""Changelog"", ""0.44 (2020-06-11)""]","[{""href"": ""https://github.com/simonw/datasette/issues/798"", ""label"": ""#798""}]" changelog:register-routes-plugin-hooks,changelog,register-routes-plugin-hooks,register_routes() plugin hooks,"Plugins can now register new views and routes via the register_routes(datasette) plugin hook ( #819 ). View functions can be defined that accept any of the current datasette object, the current request , or the ASGI scope , send and receive objects.","[""Changelog"", ""0.44 (2020-06-11)""]","[{""href"": ""https://github.com/simonw/datasette/issues/819"", ""label"": ""#819""}]" changelog:id209,changelog,id209,0.10 (2017-11-14),"Fixed #83 - 500 error on individual row pages. Stop using sqlite WITH RECURSIVE in our tests. The version of Python 3 running in Travis CI doesn't support this.","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/83"", ""label"": ""#83""}]" plugin_hooks:plugin-register-facet-classes,plugin_hooks,plugin-register-facet-classes,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]","[""Plugin hooks""]","[{""href"": ""https://github.com/simonw/datasette/issues/830"", ""label"": ""issue 830""}, {""href"": ""https://github.com/simonw/datasette/blob/main/datasette/facets.py"", ""label"": ""datasette/facets.py""}]" changelog:id59,changelog,id59,Smaller changes,"Cascading view permissions - so if a user has view-table they can view the table page even if they do not have view-database or view-instance . ( #832 ) CSRF protection no longer applies to Authentication: Bearer token requests or requests without cookies. ( #835 ) datasette.add_message() now works inside plugins. ( #864 ) Workaround for ""Too many open files"" error in test runs. ( #846 ) Respect existing scope[""actor""] if already set by ASGI middleware. ( #854 ) New process for shipping Alpha and beta releases . ( #807 ) {{ csrftoken() }} now works when plugins render a template using datasette.render_template(..., request=request) . ( #863 ) Datasette now creates a single Request object and uses it throughout the lifetime of the current HTTP request. ( #870 )","[""Changelog"", ""0.45 (2020-07-01)""]","[{""href"": ""https://github.com/simonw/datasette/issues/832"", ""label"": ""#832""}, {""href"": ""https://github.com/simonw/datasette/issues/835"", ""label"": ""#835""}, {""href"": ""https://github.com/simonw/datasette/issues/864"", ""label"": ""#864""}, {""href"": ""https://github.com/simonw/datasette/issues/846"", ""label"": ""#846""}, {""href"": ""https://github.com/simonw/datasette/issues/854"", ""label"": ""#854""}, {""href"": ""https://github.com/simonw/datasette/issues/807"", ""label"": ""#807""}, {""href"": ""https://github.com/simonw/datasette/issues/863"", ""label"": ""#863""}, {""href"": ""https://github.com/simonw/datasette/issues/870"", ""label"": ""#870""}]" changelog:log-out,changelog,log-out,Log out,"The ds_actor cookie can be used by plugins (or by Datasette's --root mechanism ) to authenticate users. The new /-/logout page provides a way to clear that cookie. A ""Log out"" button now shows in the global navigation provided the user is authenticated using the ds_actor cookie. ( #840 )","[""Changelog"", ""0.45 (2020-07-01)""]","[{""href"": ""https://github.com/simonw/datasette/issues/840"", ""label"": ""#840""}]" changelog:magic-parameters-for-canned-queries,changelog,magic-parameters-for-canned-queries,Magic parameters for canned queries,"Canned queries now support Magic parameters , which can be used to insert or select automatically generated values. For example: insert into logs (user_id, timestamp) values (:_actor_id, :_now_datetime_utc) This inserts the currently authenticated actor ID and the current datetime. ( #842 )","[""Changelog"", ""0.45 (2020-07-01)""]","[{""href"": ""https://github.com/simonw/datasette/issues/842"", ""label"": ""#842""}]" changelog:id49,changelog,id49,0.50 (2020-10-09),"The key new feature in this release is the column actions menu on the table page ( #891 ). This can be used to sort a column in ascending or descending order, facet data by that column or filter the table to just rows that have a value for that column. Plugin authors can use the new datasette.client object to make internal HTTP requests from their plugins, allowing them to make use of Datasette's JSON API. ( #943 ) New Deploying Datasette documentation with guides for deploying Datasette on a Linux server using systemd or to hosting providers that support buildpacks . ( #514 , #997 ) Other improvements in this release: Publishing to Google Cloud Run documentation now covers Google Cloud SDK options. Thanks, Geoffrey Hing. ( #995 ) New datasette -o option which opens your browser as soon as Datasette starts up. ( #970 ) Datasette now sets sqlite3.enable_callback_tracebacks(True) so that errors in custom SQL functions will display tracebacks. ( #891 ) Fixed two rendering bugs with column headers in portrait mobile view. ( #978 , #980 ) New db.table_column_details(table) introspection method for retrieving full details of the columns in a specific table, see Database introspection . Fixed a routing bug with custom page wildcard templates. ( #996 ) datasette publish heroku now deploys using Python 3.8.6. New datasette publish heroku --tar= option. ( #969 ) OPTIONS requests against HTML pages no longer return a 500 error. ( #1001 ) Datasette now supports Python 3.9. See also Datasette 0.50: The annotated release notes .","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/891"", ""label"": ""#891""}, {""href"": ""https://github.com/simonw/datasette/issues/943"", ""label"": ""#943""}, {""href"": ""https://github.com/simonw/datasette/issues/514"", ""label"": ""#514""}, {""href"": ""https://github.com/simonw/datasette/issues/997"", ""label"": ""#997""}, {""href"": ""https://github.com/simonw/datasette/pull/995"", ""label"": ""#995""}, {""href"": ""https://github.com/simonw/datasette/issues/970"", ""label"": ""#970""}, {""href"": ""https://github.com/simonw/datasette/issues/891"", ""label"": ""#891""}, {""href"": ""https://github.com/simonw/datasette/issues/978"", ""label"": ""#978""}, {""href"": ""https://github.com/simonw/datasette/issues/980"", ""label"": ""#980""}, {""href"": ""https://github.com/simonw/datasette/issues/996"", ""label"": ""#996""}, {""href"": ""https://github.com/simonw/datasette/issues/969"", ""label"": ""#969""}, {""href"": ""https://github.com/simonw/datasette/issues/1001"", ""label"": ""#1001""}, {""href"": ""https://simonwillison.net/2020/Oct/9/datasette-0-50/"", ""label"": ""Datasette 0.50: The annotated release notes""}]" changelog:url-building,changelog,url-building,URL building,"The new datasette.urls family of methods can be used to generate URLs to key pages within the Datasette interface, both within custom templates and Datasette plugins. See Building URLs within plugins for more details. ( #904 )","[""Changelog"", ""0.51 (2020-10-31)""]","[{""href"": ""https://github.com/simonw/datasette/issues/904"", ""label"": ""#904""}]" changelog:id57,changelog,id57,0.46 (2020-08-09),"This release contains a security fix related to authenticated writable canned queries. If you are using this feature you should upgrade as soon as possible. Security fix: CSRF tokens were incorrectly included in read-only canned query forms, which could allow them to be leaked to a sophisticated attacker. See issue 918 for details. Datasette now supports GraphQL via the new datasette-graphql plugin - see GraphQL in Datasette with the new datasette-graphql plugin . Principle git branch has been renamed from master to main . ( #849 ) New debugging tool: /-/allow-debug tool ( demo here ) helps test allow blocks against actors, as described in Defining permissions with ""allow"" blocks . ( #908 ) New logo for the documentation, and a new project tagline: ""An open source multi-tool for exploring and publishing data"". Whitespace in column values is now respected on display, using white-space: pre-wrap . ( #896 ) New await request.post_body() method for accessing the raw POST body, see Request object . ( #897 ) Database file downloads now include a content-length HTTP header, enabling download progress bars. ( #905 ) File downloads now also correctly set the suggested file name using a content-disposition HTTP header. ( #909 ) tests are now excluded from the Datasette package properly - thanks, abeyerpath. ( #456 ) The Datasette package published to PyPI now includes sdist as well as bdist_wheel . Better titles for canned query pages. ( #887 ) Now only loads Python files from a directory passed using the --plugins-dir option - thanks, Amjith Ramanujam. ( #890 ) New documentation section on Publishing to Vercel .","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/918"", ""label"": ""issue 918""}, {""href"": ""https://github.com/simonw/datasette-graphql"", ""label"": ""datasette-graphql""}, {""href"": ""https://simonwillison.net/2020/Aug/7/datasette-graphql/"", ""label"": ""GraphQL in Datasette with the new datasette-graphql plugin""}, {""href"": ""https://github.com/simonw/datasette/issues/849"", ""label"": ""#849""}, {""href"": ""https://latest.datasette.io/-/allow-debug"", ""label"": ""demo here""}, {""href"": ""https://github.com/simonw/datasette/issues/908"", ""label"": ""#908""}, {""href"": ""https://github.com/simonw/datasette/issues/896"", ""label"": ""#896""}, {""href"": ""https://github.com/simonw/datasette/issues/897"", ""label"": ""#897""}, {""href"": ""https://github.com/simonw/datasette/issues/905"", ""label"": ""#905""}, {""href"": ""https://github.com/simonw/datasette/issues/909"", ""label"": ""#909""}, {""href"": ""https://github.com/simonw/datasette/issues/456"", ""label"": ""#456""}, {""href"": ""https://github.com/simonw/datasette/issues/887"", ""label"": ""#887""}, {""href"": ""https://github.com/simonw/datasette/pull/890"", ""label"": ""#890""}]" changelog:id55,changelog,id55,0.47.1 (2020-08-11),Fixed a bug where the sdist distribution of Datasette was not correctly including the template files. ( #930 ),"[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/930"", ""label"": ""#930""}]" changelog:id53,changelog,id53,0.47.3 (2020-08-15),The datasette --get command-line mechanism now ensures any plugins using the startup() hook are correctly executed. ( #934 ),"[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/934"", ""label"": ""#934""}]" changelog:id26,changelog,id26,0.59 (2021-10-14),"Columns can now have associated metadata descriptions in metadata.json , see Column descriptions . ( #942 ) New register_commands() plugin hook allows plugins to register additional Datasette CLI commands, e.g. datasette mycommand file.db . ( #1449 ) Adding ?_facet_size=max to a table page now shows the number of unique values in each facet. ( #1423 ) Upgraded dependency httpx 0.20 - the undocumented allow_redirects= parameter to datasette.client is now follow_redirects= , and defaults to False where it previously defaulted to True . ( #1488 ) The --cors option now causes Datasette to return the Access-Control-Allow-Headers: Authorization header, in addition to Access-Control-Allow-Origin: * . ( #1467 ) Code that figures out which named parameters a SQL query takes in order to display form fields for them is no longer confused by strings that contain colon characters. ( #1421 ) Renamed --help-config option to --help-settings . ( #1431 ) datasette.databases property is now a documented API. ( #1443 ) The base.html template now wraps everything other than the <footer> in a <div class=""not-footer""> element, to help with advanced CSS customization. ( #1446 ) The render_cell() plugin hook can now return an awaitable function. This means the hook can execute SQL queries. ( #1425 ) register_routes(datasette) plugin hook now accepts an optional datasette argument. ( #1404 ) New hide_sql canned query option for defaulting to hiding the SQL query used by a canned query, see Additional canned query options . ( #1422 ) New --cpu option for datasette publish cloudrun . ( #1420 ) If Rich is installed in the same virtual environment as Datasette, it will be used to provide enhanced display of error tracebacks on the console. ( #1416 ) datasette.utils parse_metadata(content) function, used by the new datasette-remote-metadata plugin , is now a documented API. ( #1405 ) Fixed bug where ?_next=x&_sort=rowid could throw an error. ( #1470 ) Column cog menu no longer shows the option to facet by a column that is already selected by the default facets in metadata. ( #1469 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/942"", ""label"": ""#942""}, {""href"": ""https://github.com/simonw/datasette/issues/1449"", ""label"": ""#1449""}, {""href"": ""https://github.com/simonw/datasette/issues/1423"", ""label"": ""#1423""}, {""href"": ""https://github.com/encode/httpx/releases/tag/0.20.0"", ""label"": ""httpx 0.20""}, {""href"": ""https://github.com/simonw/datasette/issues/1488"", ""label"": ""#1488""}, {""href"": ""https://github.com/simonw/datasette/pull/1467"", ""label"": ""#1467""}, {""href"": ""https://github.com/simonw/datasette/issues/1421"", ""label"": ""#1421""}, {""href"": ""https://github.com/simonw/datasette/issues/1431"", ""label"": ""#1431""}, {""href"": ""https://github.com/simonw/datasette/issues/1443"", ""label"": ""#1443""}, {""href"": ""https://github.com/simonw/datasette/issues/1446"", ""label"": ""#1446""}, {""href"": ""https://github.com/simonw/datasette/issues/1425"", ""label"": ""#1425""}, {""href"": ""https://github.com/simonw/datasette/issues/1404"", ""label"": ""#1404""}, {""href"": ""https://github.com/simonw/datasette/issues/1422"", ""label"": ""#1422""}, {""href"": ""https://github.com/simonw/datasette/issues/1420"", ""label"": ""#1420""}, {""href"": ""https://github.com/willmcgugan/rich"", ""label"": ""Rich""}, {""href"": ""https://github.com/simonw/datasette/issues/1416"", ""label"": ""#1416""}, {""href"": ""https://datasette.io/plugins/datasette-remote-metadata"", ""label"": ""datasette-remote-metadata plugin""}, {""href"": ""https://github.com/simonw/datasette/issues/1405"", ""label"": ""#1405""}, {""href"": ""https://github.com/simonw/datasette/issues/1470"", ""label"": ""#1470""}, {""href"": ""https://github.com/simonw/datasette/issues/1469"", ""label"": ""#1469""}]" changelog:id50,changelog,id50,0.49.1 (2020-09-15),Fixed a bug with writable canned queries that use magic parameters but accept no non-magic arguments. ( #967 ),"[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/967"", ""label"": ""#967""}]" changelog:id43,changelog,id43,0.52 (2020-11-28),"This release includes a number of changes relating to an internal rebranding effort: Datasette's configuration mechanism (things like datasette --config default_page_size:10 ) has been renamed to settings . New --setting default_page_size 10 option as a replacement for --config default_page_size:10 (note the lack of a colon). The --config option is deprecated but will continue working until Datasette 1.0. ( #992 ) The /-/config introspection page is now /-/settings , and the previous page redirects to the new one. ( #1103 ) The config.json file in Configuration directory mode is now called settings.json . ( #1104 ) The undocumented datasette.config() internal method has been replaced by a documented .setting(key) method. ( #1107 ) Also in this release: New plugin hook: database_actions(datasette, actor, database, request) , which adds menu items to a new cog menu shown at the top of the database page. ( #1077 ) datasette publish cloudrun has a new --apt-get-install option that can be used to install additional Ubuntu packages as part of the deployment. This is useful for deploying the new datasette-ripgrep plugin . ( #1110 ) Swept the documentation to remove words that minimize involved difficulty. ( #1089 ) And some bug fixes: Foreign keys linking to rows with blank label columns now display as a hyphen, allowing those links to be clicked. ( #1086 ) Fixed bug where row pages could sometimes 500 if the underlying queries exceeded a time limit. ( #1088 ) Fixed a bug where the table action menu could appear partially obscured by the edge of the page. ( #1084 )","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/issues/992"", ""label"": ""#992""}, {""href"": ""https://github.com/simonw/datasette/issues/1103"", ""label"": ""#1103""}, {""href"": ""https://github.com/simonw/datasette/issues/1104"", ""label"": ""#1104""}, {""href"": ""https://github.com/simonw/datasette/issues/1107"", ""label"": ""#1107""}, {""href"": ""https://github.com/simonw/datasette/issues/1077"", ""label"": ""#1077""}, {""href"": ""https://github.com/simonw/datasette-ripgrep"", ""label"": ""datasette-ripgrep plugin""}, {""href"": ""https://github.com/simonw/datasette/issues/1110"", ""label"": ""#1110""}, {""href"": ""https://github.com/simonw/datasette/issues/1089"", ""label"": ""#1089""}, {""href"": ""https://github.com/simonw/datasette/issues/1086"", ""label"": ""#1086""}, {""href"": ""https://github.com/simonw/datasette/issues/1088"", ""label"": ""#1088""}, {""href"": ""https://github.com/simonw/datasette/issues/1084"", ""label"": ""#1084""}]" changelog:id46,changelog,id46,Smaller changes,"Wide tables shown within Datasette now scroll horizontally ( #998 ). This is achieved using a new <div class=""table-wrapper""> element which may impact the implementation of some plugins (for example this change to datasette-cluster-map ). New debug-menu permission. ( #1068 ) Removed --debug option, which didn't do anything. ( #814 ) Link: HTTP header pagination. ( #1014 ) x button for clearing filters. ( #1016 ) Edit SQL button on canned queries, ( #1019 ) --load-extension=spatialite shortcut. ( #1028 ) scale-in animation for column action menu. ( #1039 ) Option to pass a list of templates to .render_template() is now documented. ( #1045 ) New datasette.urls.static_plugins() method. ( #1033 ) datasette -o option now opens the most relevant page. ( #976 ) datasette --cors option now enables access to /database.db downloads. ( #1057 ) Database file downloads now implement cascading permissions, so you can download a database if you have view-database-download permission even if you do not have permission to access the Datasette instance. ( #1058 ) New documentation on Designing URLs for your plugin . ( #1053 )","[""Changelog"", ""0.51 (2020-10-31)""]","[{""href"": ""https://github.com/simonw/datasette/issues/998"", ""label"": ""#998""}, {""href"": ""https://github.com/simonw/datasette-cluster-map/commit/fcb4abbe7df9071c5ab57defd39147de7145b34e"", ""label"": ""this change to datasette-cluster-map""}, {""href"": ""https://github.com/simonw/datasette/issues/1068"", ""label"": ""#1068""}, {""href"": ""https://github.com/simonw/datasette/issues/814"", ""label"": ""#814""}, {""href"": ""https://github.com/simonw/datasette/issues/1014"", ""label"": ""#1014""}, {""href"": ""https://github.com/simonw/datasette/issues/1016"", ""label"": ""#1016""}, {""href"": ""https://github.com/simonw/datasette/issues/1019"", ""label"": ""#1019""}, {""href"": ""https://github.com/simonw/datasette/issues/1028"", ""label"": ""#1028""}, {""href"": ""https://github.com/simonw/datasette/issues/1039"", ""label"": ""#1039""}, {""href"": ""https://github.com/simonw/datasette/issues/1045"", ""label"": ""#1045""}, {""href"": ""https://github.com/simonw/datasette/issues/1033"", ""label"": ""#1033""}, {""href"": ""https://github.com/simonw/datasette/issues/976"", ""label"": ""#976""}, {""href"": ""https://github.com/simonw/datasette/issues/1057"", ""label"": ""#1057""}, {""href"": ""https://github.com/simonw/datasette/issues/1058"", ""label"": ""#1058""}, {""href"": ""https://github.com/simonw/datasette/issues/1053"", ""label"": ""#1053""}]" internals:internals-utils,internals,internals-utils,The datasette.utils module,"The datasette.utils module contains various utility functions used by Datasette. As a general rule you should consider anything in this module to be unstable - functions and classes here could change without warning or be removed entirely between Datasette releases, without being mentioned in the release notes. The exception to this rule is anything that is documented here. If you find a need for an undocumented utility function in your own work, consider opening an issue requesting that the function you are using be upgraded to documented and supported status.","[""Internals for plugins""]","[{""href"": ""https://github.com/simonw/datasette/issues/new"", ""label"": ""opening an issue""}]" changelog:the-road-to-datasette-1-0,changelog,the-road-to-datasette-1-0,The road to Datasette 1.0,"I've assembled a milestone for Datasette 1.0 . The focus of the 1.0 release will be the following: Signify confidence in the quality/stability of Datasette Give plugin authors confidence that their plugins will work for the whole 1.x release cycle Provide the same confidence to developers building against Datasette JSON APIs If you have thoughts about what you would like to see for Datasette 1.0 you can join the conversation on issue #519 .","[""Changelog"", ""0.44 (2020-06-11)""]","[{""href"": ""https://github.com/simonw/datasette/milestone/7"", ""label"": ""milestone for Datasette 1.0""}, {""href"": ""https://github.com/simonw/datasette/issues/519"", ""label"": ""the conversation on issue #519""}]" changelog:v0-28-register-output-renderer,changelog,v0-28-register-output-renderer,register_output_renderer plugins,"Russ Garrett implemented a new Datasette plugin hook called register_output_renderer ( #441 ) which allows plugins to create additional output renderers in addition to Datasette's default .json and .csv . Russ's in-development datasette-geo plugin includes an example of this hook being used to output .geojson automatically converted from SpatiaLite.","[""Changelog"", ""0.28 (2019-05-19)""]","[{""href"": ""https://github.com/simonw/datasette/pull/441"", ""label"": ""#441""}, {""href"": ""https://github.com/russss/datasette-geo"", ""label"": ""datasette-geo""}, {""href"": ""https://github.com/russss/datasette-geo/blob/d4cecc020848bbde91e9e17bf352f7c70bc3dccf/datasette_plugin_geo/geojson.py"", ""label"": ""an example""}]" changelog:id77,changelog,id77,0.31 (2019-11-11),"This version adds compatibility with Python 3.8 and breaks compatibility with Python 3.5. If you are still running Python 3.5 you should stick with 0.30.2 , which you can install like this: pip install datasette==0.30.2 Format SQL button now works with read-only SQL queries - thanks, Tobias Kunze ( #602 ) New ?column__notin=x,y,z filter for table views ( #614 ) Table view now uses select col1, col2, col3 instead of select * Database filenames can now contain spaces - thanks, Tobias Kunze ( #590 ) Removed obsolete ?_group_count=col feature ( #504 ) Improved user interface and documentation for datasette publish cloudrun ( #608 ) Tables with indexes now show the CREATE INDEX statements on the table page ( #618 ) Current version of uvicorn is now shown on /-/versions Python 3.8 is now supported! ( #622 ) Python 3.5 is no longer supported.","[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/pull/602"", ""label"": ""#602""}, {""href"": ""https://github.com/simonw/datasette/issues/614"", ""label"": ""#614""}, {""href"": ""https://github.com/simonw/datasette/pull/590"", ""label"": ""#590""}, {""href"": ""https://github.com/simonw/datasette/issues/504"", ""label"": ""#504""}, {""href"": ""https://github.com/simonw/datasette/issues/608"", ""label"": ""#608""}, {""href"": ""https://github.com/simonw/datasette/issues/618"", ""label"": ""#618""}, {""href"": ""https://www.uvicorn.org/"", ""label"": ""uvicorn""}, {""href"": ""https://github.com/simonw/datasette/issues/622"", ""label"": ""#622""}]" changelog:id76,changelog,id76,0.31.1 (2019-11-12),Deployments created using datasette publish now use python:3.8 base Docker image ( #629 ),"[""Changelog""]","[{""href"": ""https://github.com/simonw/datasette/pull/629"", ""label"": ""#629""}]" contributing:contributing-bug-fix-branch,contributing,contributing-bug-fix-branch,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","[""Contributing""]","[{""href"": ""https://github.com/simonw/datasette/releases/new"", ""label"": ""Draft a new release""}]" plugin_hooks:plugin-hook-publish-subcommand,plugin_hooks,plugin-hook-publish-subcommand,publish_subcommand(publish),"publish - Click publish command group The Click command group for the datasette publish subcommand This hook allows you to create new providers for the datasette publish command. Datasette uses this hook internally to implement the default cloudrun and heroku subcommands, so you can read their source to see examples of this hook in action. Let's say you want to build a plugin that adds a datasette publish my_hosting_provider --api_key=xxx mydatabase.db publish command. Your implementation would start like this: from datasette import hookimpl from datasette.publish.common import ( add_common_publish_arguments_and_options, ) import click @hookimpl def publish_subcommand(publish): @publish.command() @add_common_publish_arguments_and_options @click.option( ""-k"", ""--api_key"", help=""API key for talking to my hosting provider"", ) def my_hosting_provider( files, metadata, extra_options, branch, template_dir, plugins_dir, static, install, plugin_secret, version_note, secret, title, license, license_url, source, source_url, about, about_url, api_key, ): ... Examples: datasette-publish-fly , datasette-publish-vercel","[""Plugin hooks""]","[{""href"": ""https://github.com/simonw/datasette/tree/main/datasette/publish"", ""label"": ""their source""}, {""href"": ""https://datasette.io/plugins/datasette-publish-fly"", ""label"": ""datasette-publish-fly""}, {""href"": ""https://datasette.io/plugins/datasette-publish-vercel"", ""label"": ""datasette-publish-vercel""}]" changelog:id115,changelog,id115,0.22.1 (2018-05-23),"Bugfix release, plus we now use versioneer for our version numbers. Faceting no longer breaks pagination, fixes #282 Add __version_info__ derived from __version__ [Robert Gieseke] This might be tuple of more than two values (major and minor version) if commits have been made after a release. Add version number support with Versioneer. [Robert Gieseke] Versioneer Licence: Public Domain (CC0-1.0) Closes #273 Refactor inspect logic [Russ Garrett]","[""Changelog""]","[{""href"": ""https://github.com/warner/python-versioneer"", ""label"": ""versioneer""}, {""href"": ""https://github.com/simonw/datasette/issues/282"", ""label"": ""#282""}, {""href"": ""https://github.com/simonw/datasette/issues/273"", ""label"": ""#273""}]" getting_started:getting-started-glitch,getting_started,getting-started-glitch,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 .","[""Getting started""]","[{""href"": ""https://glitch.com/"", ""label"": ""Glitch""}, {""href"": ""https://glitch.com/~datasette-csvs"", ""label"": ""glitch.com/~datasette-csvs""}, {""href"": ""https://glitch.com/edit/#!/remix/datasette-csvs"", ""label"": null}, {""href"": ""https://github.com/simonw/sqlite-utils"", ""label"": ""sqlite-utils""}, {""href"": ""https://data.seattle.gov/Community/Public-Art-Data/j7sn-tdzk"", ""label"": ""Public Art Data""}, {""href"": ""https://simonwillison.net/2019/Apr/23/datasette-glitch/"", ""label"": ""Running Datasette on Glitch""}]" getting_started:getting-started-demo,getting_started,getting-started-demo,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 .","[""Getting started""]","[{""href"": ""https://global-power-plants.datasettes.com/global-power-plants/global-power-plants"", ""label"": ""global-power-plants.datasettes.com""}, {""href"": ""https://www.wri.org/publication/global-power-plant-database"", ""label"": ""World Resources Institude""}, {""href"": ""https://github.com/simonw/datasette-cluster-map"", ""label"": ""datasette-cluster-map""}, {""href"": ""https://fivethirtyeight.datasettes.com/fivethirtyeight"", ""label"": ""fivethirtyeight.datasettes.com""}, {""href"": ""https://github.com/fivethirtyeight/data"", ""label"": ""FiveThirtyEight GitHub repository""}]" deploying:apache-proxy-configuration,deploying,apache-proxy-configuration,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.","[""Deploying Datasette"", ""Running Datasette behind a proxy""]","[{""href"": ""https://httpd.apache.org/"", ""label"": ""Apache""}, {""href"": ""https://datasette-apache-proxy-demo.datasette.io/prefix/"", ""label"": ""datasette-apache-proxy-demo.datasette.io/prefix/""}, {""href"": ""https://github.com/simonw/datasette/tree/main/demos/apache-proxy"", ""label"": ""demos/apache-proxy""}, {""href"": ""https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#proxypreservehost"", ""label"": ""ProxyPreserveHost On""}]" changelog:id67,changelog,id67,0.38 (2020-03-08),"The Docker build of Datasette now uses SQLite 3.31.1, upgraded from 3.26. ( #695 ) datasette publish cloudrun now accepts an optional --memory=2Gi flag for setting the Cloud Run allocated memory to a value other than the default (256Mi). ( #694 ) Fixed bug where templates that shipped with plugins were sometimes not being correctly loaded. ( #697 )","[""Changelog""]","[{""href"": ""https://hub.docker.com/r/datasetteproject/datasette"", ""label"": ""Docker build""}, {""href"": ""https://github.com/simonw/datasette/issues/695"", ""label"": ""#695""}, {""href"": ""https://github.com/simonw/datasette/issues/694"", ""label"": ""#694""}, {""href"": ""https://github.com/simonw/datasette/issues/697"", ""label"": ""#697""}]" changelog:id54,changelog,id54,0.47.2 (2020-08-12),Fixed an issue with the Docker image published to Docker Hub . ( #931 ),"[""Changelog""]","[{""href"": ""https://hub.docker.com/r/datasetteproject/datasette"", ""label"": ""published to Docker Hub""}, {""href"": ""https://github.com/simonw/datasette/issues/931"", ""label"": ""#931""}]" installation:installation-docker,installation,installation-docker,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","[""Installation"", ""Advanced installation options""]","[{""href"": ""https://hub.docker.com/r/datasetteproject/datasette/"", ""label"": ""https://hub.docker.com/r/datasetteproject/datasette/""}, {""href"": ""https://www.docker.com/docker-mac"", ""label"": ""Docker for Mac""}, {""href"": ""http://127.0.0.1:8001/"", ""label"": ""http://127.0.0.1:8001/""}, {""href"": ""https://latest.datasette.io/fixtures.db"", ""label"": ""https://latest.datasette.io/fixtures.db""}]" internals:datasette-sign,internals,datasette-sign,".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"") .","[""Internals for plugins"", ""Datasette class""]","[{""href"": ""https://itsdangerous.palletsprojects.com/en/1.1.x/serializer/#the-salt"", ""label"": ""itsdangerous salt documentation""}, {""href"": ""https://itsdangerous.palletsprojects.com/"", ""label"": ""itsdangerous""}]" changelog:id71,changelog,id71,0.35 (2020-02-04),"Added five new plugins and one new conversion tool to the The Datasette Ecosystem . The Datasette class has a new render_template() method which can be used by plugins to render templates using Datasette's pre-configured Jinja templating library. You can now execute SQL queries that start with a -- comment - thanks, Jay Graves ( #653 )","[""Changelog""]","[{""href"": ""https://jinja.palletsprojects.com/"", ""label"": ""Jinja""}, {""href"": ""https://github.com/simonw/datasette/pull/653"", ""label"": ""#653""}]" changelog:id74,changelog,id74,0.32 (2019-11-14),"Datasette now renders templates using Jinja async mode . This means plugins can provide custom template functions that perform asynchronous actions, for example the new datasette-template-sql plugin which allows custom templates to directly execute SQL queries and render their results. ( #628 )","[""Changelog""]","[{""href"": ""https://jinja.palletsprojects.com/en/2.10.x/api/#async-support"", ""label"": ""Jinja async mode""}, {""href"": ""https://github.com/simonw/datasette-template-sql"", ""label"": ""datasette-template-sql""}, {""href"": ""https://github.com/simonw/datasette/issues/628"", ""label"": ""#628""}]" plugin_hooks:plugin-hook-extra-template-vars,plugin_hooks,plugin-hook-extra-template-vars,"extra_template_vars(template, database, table, columns, view_name, request, datasette)","Extra template variables that should be made available in the rendered template context. template - string The template that is being rendered, e.g. database.html database - string or None The name of the database, or None if the page does not correspond to a database (e.g. the root page) table - string or None The name of the table, or None if the page does not correct to a table columns - list of strings or None The names of the database columns that will be displayed on this page. None if the page does not contain a table. view_name - string The name of the view being displayed. ( index , database , table , and row are the most important ones.) request - 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","[""Plugin hooks"", ""Page extras""]","[{""href"": ""https://jinja.palletsprojects.com/en/2.10.x/api/#async-support"", ""label"": ""async mode""}, {""href"": ""https://datasette.io/plugins/datasette-search-all"", ""label"": ""datasette-search-all""}, {""href"": ""https://datasette.io/plugins/datasette-template-sql"", ""label"": ""datasette-template-sql""}]" internals:datasette-render-template,internals,datasette-render-template,"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.","[""Internals for plugins"", ""Datasette class""]","[{""href"": ""https://jinja.palletsprojects.com/en/2.11.x/api/#jinja2.Template"", ""label"": ""Template object""}, {""href"": ""https://jinja.palletsprojects.com/en/2.11.x/"", ""label"": ""Jinja template""}]" plugin_hooks:plugin-hook-jinja2-environment-from-request,plugin_hooks,plugin-hook-jinja2-environment-from-request,"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.","[""Plugin hooks""]","[{""href"": ""https://jinja.palletsprojects.com/en/3.0.x/api/#jinja2.Environment"", ""label"": ""Jinja environment""}, {""href"": ""https://jinja.palletsprojects.com/en/3.0.x/api/#jinja2.Environment.overlay"", ""label"": ""overlay() method""}]" changelog:latest-datasette-io,changelog,latest-datasette-io,latest.datasette.io,"Every commit to Datasette master is now automatically deployed by Travis CI to https://latest.datasette.io/ - ensuring there is always a live demo of the latest version of the software. The demo uses the fixtures from our unit tests, ensuring it demonstrates the same range of functionality that is covered by the tests. You can see how the deployment mechanism works in our .travis.yml file.","[""Changelog"", ""0.23 (2018-06-18)""]","[{""href"": ""https://latest.datasette.io/"", ""label"": ""https://latest.datasette.io/""}, {""href"": ""https://github.com/simonw/datasette/blob/master/tests/fixtures.py"", ""label"": ""the fixtures""}, {""href"": ""https://github.com/simonw/datasette/blob/master/.travis.yml"", ""label"": "".travis.yml""}]" authentication:permissions-view-instance,authentication,permissions-view-instance,view-instance,"Top level permission - Actor is allowed to view any pages within this instance, starting at https://latest.datasette.io/ Default allow .","[""Authentication and permissions"", ""Built-in permissions""]","[{""href"": ""https://latest.datasette.io/"", ""label"": ""https://latest.datasette.io/""}]" contributing:contributing-continuous-deployment,contributing,contributing-continuous-deployment,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.","[""Contributing""]","[{""href"": ""https://latest.datasette.io/"", ""label"": ""latest.datasette.io""}, {""href"": ""https://github.com/simonw/datasette/blob/main/.github/workflows/deploy-latest.yml"", ""label"": "".github/workflows/deploy-latest.yml""}]" authentication:allowdebugview,authentication,allowdebugview,The /-/allow-debug tool,"The /-/allow-debug tool lets you try out different ""action"" blocks against different ""actor"" JSON objects. You can try that out here: https://latest.datasette.io/-/allow-debug","[""Authentication and permissions"", ""Permissions""]","[{""href"": ""https://latest.datasette.io/-/allow-debug"", ""label"": ""https://latest.datasette.io/-/allow-debug""}]" authentication:authentication-permissions-allow,authentication,authentication-permissions-allow,"Defining permissions with ""allow"" blocks","The standard way to define permissions in Datasette is to use an ""allow"" block in the datasette.yaml file . This is a JSON document describing which actors are allowed to perform a permission. The most basic form of allow block is this ( allow demo , deny demo ): [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" allow: id: root """""").strip(), ""YAML"", ""JSON"" ) ]]] [[[end]]] This will match any actors with an ""id"" property of ""root"" - for example, an actor that looks like this: { ""id"": ""root"", ""name"": ""Root User"" } An allow block can specify ""deny all"" using false ( demo ): [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" allow: false """""").strip(), ""YAML"", ""JSON"" ) ]]] [[[end]]] An ""allow"" of true allows all access ( demo ): [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" allow: true """""").strip(), ""YAML"", ""JSON"" ) ]]] [[[end]]] Allow keys can provide a list of values. These will match any actor that has any of those values ( allow demo , deny demo ): [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" allow: id: - simon - cleopaws """""").strip(), ""YAML"", ""JSON"" ) ]]] [[[end]]] This will match any actor with an ""id"" of either ""simon"" or ""cleopaws"" . Actors can have properties that feature a list of values. These will be matched against the list of values in an allow block. Consider the following actor: { ""id"": ""simon"", ""roles"": [""staff"", ""developer""] } This allow block will provide access to any actor that has ""developer"" as one of their roles ( allow demo , deny demo ): [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" allow: roles: - developer """""").strip(), ""YAML"", ""JSON"" ) ]]] [[[end]]] Note that ""roles"" is not a concept that is baked into Datasette - it's a convention that plugins can choose to implement and act on. If you want to provide access to any actor with a value for a specific key, use ""*"" . For example, to match any logged-in user specify the following ( allow demo , deny demo ): [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" allow: id: ""*"" """""").strip(), ""YAML"", ""JSON"" ) ]]] [[[end]]] You can specify that only unauthenticated actors (from anonymous HTTP requests) should be allowed access using the special ""unauthenticated"": true key in an allow block ( allow demo , deny demo ): [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" allow: unauthenticated: true """""").strip(), ""YAML"", ""JSON"" ) ]]] [[[end]]] Allow keys act as an ""or"" mechanism. An actor will be able to execute the query if any of their JSON properties match any of the values in the corresponding lists in the allow block. The following block will allow users with either a role of ""ops"" OR users who have an id of ""simon"" or ""cleopaws"" : [[[cog from metadata_doc import config_example import textwrap config_example(cog, textwrap.dedent( """""" allow: id: - simon - cleopaws role: ops """""").strip(), ""YAML"", ""JSON"" ) ]]] [[[end]]] Demo for cleopaws , demo for ops role , demo for an actor matching neither rule .","[""Authentication and permissions"", ""Permissions""]","[{""href"": ""https://latest.datasette.io/-/allow-debug?actor=%7B%22id%22%3A+%22root%22%7D&allow=%7B%0D%0A++++++++%22id%22%3A+%22root%22%0D%0A++++%7D"", ""label"": ""allow demo""}, {""href"": ""https://latest.datasette.io/-/allow-debug?actor=%7B%22id%22%3A+%22trevor%22%7D&allow=%7B%0D%0A++++++++%22id%22%3A+%22root%22%0D%0A++++%7D"", ""label"": ""deny demo""}, {""href"": ""https://latest.datasette.io/-/allow-debug?actor=%7B%0D%0A++++%22id%22%3A+%22root%22%0D%0A%7D&allow=false"", ""label"": ""demo""}, {""href"": ""https://latest.datasette.io/-/allow-debug?actor=%7B%0D%0A++++%22id%22%3A+%22root%22%0D%0A%7D&allow=true"", ""label"": ""demo""}, {""href"": ""https://latest.datasette.io/-/allow-debug?actor=%7B%0D%0A++++%22id%22%3A+%22cleopaws%22%0D%0A%7D&allow=%7B%0D%0A++++%22id%22%3A+%5B%0D%0A++++++++%22simon%22%2C%0D%0A++++++++%22cleopaws%22%0D%0A++++%5D%0D%0A%7D"", ""label"": ""allow demo""}, {""href"": ""https://latest.datasette.io/-/allow-debug?actor=%7B%0D%0A++++%22id%22%3A+%22pancakes%22%0D%0A%7D&allow=%7B%0D%0A++++%22id%22%3A+%5B%0D%0A++++++++%22simon%22%2C%0D%0A++++++++%22cleopaws%22%0D%0A++++%5D%0D%0A%7D"", ""label"": ""deny demo""}, {""href"": ""https://latest.datasette.io/-/allow-debug?actor=%7B%0D%0A++++%22id%22%3A+%22simon%22%2C%0D%0A++++%22roles%22%3A+%5B%0D%0A++++++++%22staff%22%2C%0D%0A++++++++%22developer%22%0D%0A++++%5D%0D%0A%7D&allow=%7B%0D%0A++++%22roles%22%3A+%5B%0D%0A++++++++%22developer%22%0D%0A++++%5D%0D%0A%7D"", ""label"": ""allow demo""}, {""href"": ""https://latest.datasette.io/-/allow-debug?actor=%7B%0D%0A++++%22id%22%3A+%22cleopaws%22%2C%0D%0A++++%22roles%22%3A+%5B%22dog%22%5D%0D%0A%7D&allow=%7B%0D%0A++++%22roles%22%3A+%5B%0D%0A++++++++%22developer%22%0D%0A++++%5D%0D%0A%7D"", ""label"": ""deny demo""}, {""href"": ""https://latest.datasette.io/-/allow-debug?actor=%7B%0D%0A++++%22id%22%3A+%22simon%22%0D%0A%7D&allow=%7B%0D%0A++++%22id%22%3A+%22*%22%0D%0A%7D"", ""label"": ""allow demo""}, {""href"": ""https://latest.datasette.io/-/allow-debug?actor=%7B%0D%0A++++%22bot%22%3A+%22readme-bot%22%0D%0A%7D&allow=%7B%0D%0A++++%22id%22%3A+%22*%22%0D%0A%7D"", ""label"": ""deny demo""}, {""href"": ""https://latest.datasette.io/-/allow-debug?actor=null&allow=%7B%0D%0A++++%22unauthenticated%22%3A+true%0D%0A%7D"", ""label"": ""allow demo""}, {""href"": ""https://latest.datasette.io/-/allow-debug?actor=%7B%0D%0A++++%22id%22%3A+%22hello%22%0D%0A%7D&allow=%7B%0D%0A++++%22unauthenticated%22%3A+true%0D%0A%7D"", ""label"": ""deny demo""}, {""href"": ""https://latest.datasette.io/-/allow-debug?actor=%7B%0D%0A++++%22id%22%3A+%22cleopaws%22%0D%0A%7D&allow=%7B%0D%0A++++%22id%22%3A+%5B%0D%0A++++++++%22simon%22%2C%0D%0A++++++++%22cleopaws%22%0D%0A++++%5D%2C%0D%0A++++%22role%22%3A+%22ops%22%0D%0A%7D"", ""label"": ""Demo for cleopaws""}, {""href"": ""https://latest.datasette.io/-/allow-debug?actor=%7B%0D%0A++++%22id%22%3A+%22trevor%22%2C%0D%0A++++%22role%22%3A+%5B%0D%0A++++++++%22ops%22%2C%0D%0A++++++++%22staff%22%0D%0A++++%5D%0D%0A%7D&allow=%7B%0D%0A++++%22id%22%3A+%5B%0D%0A++++++++%22simon%22%2C%0D%0A++++++++%22cleopaws%22%0D%0A++++%5D%2C%0D%0A++++%22role%22%3A+%22ops%22%0D%0A%7D"", ""label"": ""demo for ops role""}, {""href"": ""https://latest.datasette.io/-/allow-debug?actor=%7B%0D%0A++++%22id%22%3A+%22percy%22%2C%0D%0A++++%22role%22%3A+%5B%0D%0A++++++++%22staff%22%0D%0A++++%5D%0D%0A%7D&allow=%7B%0D%0A++++%22id%22%3A+%5B%0D%0A++++++++%22simon%22%2C%0D%0A++++++++%22cleopaws%22%0D%0A++++%5D%2C%0D%0A++++%22role%22%3A+%22ops%22%0D%0A%7D"", ""label"": ""demo for an actor matching neither rule""}]" introspection:jsondataview-config,introspection,jsondataview-config,/-/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 .","[""Introspection""]","[{""href"": ""https://latest.datasette.io/-/config"", ""label"": ""Config example""}]" introspection:jsondataview-databases,introspection,jsondataview-databases,/-/databases,"Shows currently attached databases. Databases example : [ { ""hash"": null, ""is_memory"": false, ""is_mutable"": true, ""name"": ""fixtures"", ""path"": ""fixtures.db"", ""size"": 225280 } ]","[""Introspection""]","[{""href"": ""https://latest.datasette.io/-/databases"", ""label"": ""Databases example""}]" introspection:jsondataview-threads,introspection,jsondataview-threads,/-/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"": [ ""<Task pending coro=<RequestResponseCycle.run_asgi() running at uvicorn/protocols/http/httptools_impl.py:385> cb=[set.discard()]>"", ""<Task pending coro=<Server.serve() running at uvicorn/main.py:361> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x10365c3d0>()]> cb=[run_until_complete.<locals>.<lambda>()]>"", ""<Task pending coro=<LifespanOn.main() running at uvicorn/lifespan/on.py:48> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x10364f050>()]>>"" ] }","[""Introspection""]","[{""href"": ""https://latest.datasette.io/-/threads"", ""label"": ""Threads example""}]" introspection:jsondataview-versions,introspection,jsondataview-versions,/-/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"" } }","[""Introspection""]","[{""href"": ""https://latest.datasette.io/-/versions"", ""label"": ""Versions example""}]" settings:setting-template-debug,settings,setting-template-debug,template_debug,"This setting enables template context debug mode, which is useful to help understand what variables are available to custom templates when you are writing them. Enable it like this: datasette mydatabase.db --setting template_debug 1 Now you can add ?_context=1 or &_context=1 to any Datasette page to see the context that was passed to that template. Some examples: https://latest.datasette.io/?_context=1 https://latest.datasette.io/fixtures?_context=1 https://latest.datasette.io/fixtures/roadside_attractions?_context=1","[""Settings"", ""Settings""]","[{""href"": ""https://latest.datasette.io/?_context=1"", ""label"": ""https://latest.datasette.io/?_context=1""}, {""href"": ""https://latest.datasette.io/fixtures?_context=1"", ""label"": ""https://latest.datasette.io/fixtures?_context=1""}, {""href"": ""https://latest.datasette.io/fixtures/roadside_attractions?_context=1"", ""label"": ""https://latest.datasette.io/fixtures/roadside_attractions?_context=1""}]" settings:setting-trace-debug,settings,setting-trace-debug,trace_debug,"This setting enables appending ?_trace=1 to any page in order to see the SQL queries and other trace information that was used to generate that page. Enable it like this: datasette mydatabase.db --setting trace_debug 1 Some examples: https://latest.datasette.io/?_trace=1 https://latest.datasette.io/fixtures/roadside_attractions?_trace=1 See datasette.tracer for details on how to hook into this mechanism as a plugin author.","[""Settings"", ""Settings""]","[{""href"": ""https://latest.datasette.io/?_trace=1"", ""label"": ""https://latest.datasette.io/?_trace=1""}, {""href"": ""https://latest.datasette.io/fixtures/roadside_attractions?_trace=1"", ""label"": ""https://latest.datasette.io/fixtures/roadside_attractions?_trace=1""}]" sql_queries:id3,sql_queries,id3,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 .","[""Running SQL queries""]","[{""href"": ""https://latest.datasette.io/_memory?sql=select%0D%0A++%27fixtures%27+as+database%2C+*%0D%0Afrom%0D%0A++%5Bfixtures%5D.sqlite_master%0D%0Aunion%0D%0Aselect%0D%0A++%27extra_database%27+as+database%2C+*%0D%0Afrom%0D%0A++%5Bextra_database%5D.sqlite_master"", ""label"": ""Try that out here""}]" authentication:permissions-view-database,authentication,permissions-view-database,view-database,"Actor is allowed to view a database page, e.g. https://latest.datasette.io/fixtures resource - string The name of the database Default allow .","[""Authentication and permissions"", ""Built-in permissions""]","[{""href"": ""https://latest.datasette.io/fixtures"", ""label"": ""https://latest.datasette.io/fixtures""}]" authentication:authentication-permissions-execute-sql,authentication,authentication-permissions-execute-sql,Controlling the ability to execute arbitrary SQL,"Datasette defaults to allowing any site visitor to execute their own custom SQL queries, for example using the form on the database page or by appending a ?_where= parameter to the table page like this . Access to this ability is controlled by the execute-sql permission. The easiest way to disable arbitrary SQL queries is using the default_allow_sql setting when you first start Datasette running. You can alternatively use an ""allow_sql"" block to control who is allowed to execute arbitrary SQL queries. To prevent any user from executing arbitrary SQL queries, use this: [[[cog config_example(cog, """""" allow_sql: false """""") ]]] [[[end]]] To enable just the root user to execute SQL for all databases in your instance, use the following: [[[cog config_example(cog, """""" allow_sql: id: root """""") ]]] [[[end]]] To limit this ability for just one specific database, use this: [[[cog config_example(cog, """""" databases: mydatabase: allow_sql: id: root """""") ]]] [[[end]]]","[""Authentication and permissions"", ""Access permissions in ""]","[{""href"": ""https://latest.datasette.io/fixtures"", ""label"": ""the database page""}, {""href"": ""https://latest.datasette.io/fixtures/facetable?_where=_city_id=1"", ""label"": ""like this""}]" authentication:permissions-view-database-download,authentication,permissions-view-database-download,view-database-download,"Actor is allowed to download a database, e.g. https://latest.datasette.io/fixtures.db resource - string The name of the database Default allow .","[""Authentication and permissions"", ""Built-in permissions""]","[{""href"": ""https://latest.datasette.io/fixtures.db"", ""label"": ""https://latest.datasette.io/fixtures.db""}]" changelog:miscellaneous,changelog,miscellaneous,Miscellaneous,"Got JSON data in one of your columns? Use the new ?_json=COLNAME argument to tell Datasette to return that JSON value directly rather than encoding it as a string. If you just want an array of the first value of each row, use the new ?_shape=arrayfirst option - example .","[""Changelog"", ""0.23 (2018-06-18)""]","[{""href"": ""https://latest.datasette.io/fixtures.json?sql=select+neighborhood+from+facetable+order+by+pk+limit+101&_shape=arrayfirst"", ""label"": ""example""}]" binary_data:binary,binary_data,binary,Binary data,"SQLite tables can contain binary data in BLOB columns. Datasette includes special handling for these binary values. The Datasette interface detects binary values and provides a link to download their content, for example on https://latest.datasette.io/fixtures/binary_data Binary data is represented in .json exports using Base64 encoding. https://latest.datasette.io/fixtures/binary_data.json?_shape=array [ { ""rowid"": 1, ""data"": { ""$base64"": true, ""encoded"": ""FRwCx60F/g=="" } }, { ""rowid"": 2, ""data"": { ""$base64"": true, ""encoded"": ""FRwDx60F/g=="" } }, { ""rowid"": 3, ""data"": null } ]",[],"[{""href"": ""https://latest.datasette.io/fixtures/binary_data"", ""label"": ""https://latest.datasette.io/fixtures/binary_data""}, {""href"": ""https://latest.datasette.io/fixtures/binary_data.json?_shape=array"", ""label"": ""https://latest.datasette.io/fixtures/binary_data.json?_shape=array""}]" binary_data:binary-linking,binary_data,binary-linking,Linking to binary downloads,"The .blob output format is used to return binary data. It requires a _blob_column= query string argument specifying which BLOB column should be downloaded, for example: https://latest.datasette.io/fixtures/binary_data/1.blob?_blob_column=data This output format can also be used to return binary data from an arbitrary SQL query. Since such queries do not specify an exact row, an additional ?_blob_hash= parameter can be used to specify the SHA-256 hash of the value that is being linked to. Consider the query select data from binary_data - demonstrated here . That page links to the binary value downloads. Those links look like this: https://latest.datasette.io/fixtures.blob?sql=select+data+from+binary_data&_blob_column=data&_blob_hash=f3088978da8f9aea479ffc7f631370b968d2e855eeb172bea7f6c7a04262bb6d These .blob links are also returned in the .csv exports Datasette provides for binary tables and queries, since the CSV format does not have a mechanism for representing binary data.","[""Binary data""]","[{""href"": ""https://latest.datasette.io/fixtures/binary_data/1.blob?_blob_column=data"", ""label"": ""https://latest.datasette.io/fixtures/binary_data/1.blob?_blob_column=data""}, {""href"": ""https://latest.datasette.io/fixtures?sql=select+data+from+binary_data"", ""label"": ""demonstrated here""}, {""href"": ""https://latest.datasette.io/fixtures.blob?sql=select+data+from+binary_data&_blob_column=data&_blob_hash=f3088978da8f9aea479ffc7f631370b968d2e855eeb172bea7f6c7a04262bb6d"", ""label"": ""https://latest.datasette.io/fixtures.blob?sql=select+data+from+binary_data&_blob_column=data&_blob_hash=f3088978da8f9aea479ffc7f631370b968d2e855eeb172bea7f6c7a04262bb6d""}]" authentication:permissions-view-table,authentication,permissions-view-table,view-table,"Actor is allowed to view a table (or view) page, e.g. https://latest.datasette.io/fixtures/complex_foreign_keys resource - tuple: (string, string) The name of the database, then the name of the table Default allow .","[""Authentication and permissions"", ""Built-in permissions""]","[{""href"": ""https://latest.datasette.io/fixtures/complex_foreign_keys"", ""label"": ""https://latest.datasette.io/fixtures/complex_foreign_keys""}]" csv_export:id1,csv_export,id1,CSV export,"Any Datasette table, view or custom SQL query can be exported as CSV. To obtain the CSV representation of the table you are looking, click the ""this data as CSV"" link. You can also use the advanced export form for more control over the resulting file, which looks like this and has the following options: download file - instead of displaying CSV in your browser, this forces your browser to download the CSV to your downloads directory. expand labels - if your table has any foreign key references this option will cause the CSV to gain additional COLUMN_NAME_label columns with a label for each foreign key derived from the linked table. In this example the city_id column is accompanied by a city_id_label column. stream all rows - by default CSV files only contain the first max_returned_rows records. This option will cause Datasette to loop through every matching record and return them as a single CSV file. You can try that out on https://latest.datasette.io/fixtures/facetable?_size=4",[],"[{""href"": ""https://latest.datasette.io/fixtures/facetable.csv?_labels=on&_size=max"", ""label"": ""In this example""}, {""href"": ""https://latest.datasette.io/fixtures/facetable?_size=4"", ""label"": ""https://latest.datasette.io/fixtures/facetable?_size=4""}]" facets:id2,facets,id2,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","[""Facets""]","[{""href"": ""https://latest.datasette.io/fixtures/facetable?_facet_array=tags"", ""label"": ""latest.datasette.io/fixtures/facetable?_facet_array=tags""}]" facets:id3,facets,id3,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","[""Facets""]","[{""href"": ""https://latest.datasette.io/fixtures/facetable?_facet_date=created"", ""label"": ""latest.datasette.io/fixtures/facetable?_facet_date=created""}]" internals:internals-tracer,internals,internals-tracer,datasette.tracer,"Running Datasette with --setting trace_debug 1 enables trace debug output, which can then be viewed by adding ?_trace=1 to the query string for any page. You can see an example of this at the bottom of latest.datasette.io/fixtures/facetable?_trace=1 . The JSON output shows full details of every SQL query that was executed to generate the page. The datasette-pretty-traces plugin can be installed to provide a more readable display of this information. You can see a demo of that here . You can add your own custom traces to the JSON output using the trace() context manager. This takes a string that identifies the type of trace being recorded, and records any keyword arguments as additional JSON keys on the resulting trace object. The start and end time, duration and a traceback of where the trace was executed will be automatically attached to the JSON object. This example uses trace to record the start, end and duration of any HTTP GET requests made using the function: from datasette.tracer import trace import httpx async def fetch_url(url): with trace(""fetch-url"", url=url): async with httpx.AsyncClient() as client: return await client.get(url)","[""Internals for plugins""]","[{""href"": ""https://latest.datasette.io/fixtures/facetable?_trace=1"", ""label"": ""latest.datasette.io/fixtures/facetable?_trace=1""}, {""href"": ""https://datasette.io/plugins/datasette-pretty-traces"", ""label"": ""datasette-pretty-traces""}, {""href"": ""https://latest-with-plugins.datasette.io/github/commits?_trace=1"", ""label"": ""a demo of that here""}]" changelog:writable-canned-queries,changelog,writable-canned-queries,Writable canned queries,"Datasette's Canned queries feature lets you define SQL queries in metadata.json which can then be executed by users visiting a specific URL. https://latest.datasette.io/fixtures/neighborhood_search for example. Canned queries were previously restricted to SELECT , but Datasette 0.44 introduces the ability for canned queries to execute INSERT or UPDATE queries as well, using the new ""write"": true property ( #800 ): { ""databases"": { ""dogs"": { ""queries"": { ""add_name"": { ""sql"": ""INSERT INTO names (name) VALUES (:name)"", ""write"": true } } } } } See Writable canned queries for more details.","[""Changelog"", ""0.44 (2020-06-11)""]","[{""href"": ""https://latest.datasette.io/fixtures/neighborhood_search"", ""label"": ""https://latest.datasette.io/fixtures/neighborhood_search""}, {""href"": ""https://github.com/simonw/datasette/issues/800"", ""label"": ""#800""}]" sql_queries:canned-queries-named-parameters,sql_queries,canned-queries-named-parameters,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]]]","[""Running SQL queries"", ""Canned queries""]","[{""href"": ""https://latest.datasette.io/fixtures/neighborhood_search?text=town"", ""label"": ""https://latest.datasette.io/fixtures/neighborhood_search?text=town""}]" authentication:permissions-view-query,authentication,permissions-view-query,view-query,"Actor is allowed to view (and execute) a canned query page, e.g. https://latest.datasette.io/fixtures/pragma_cache_size - this includes executing Writable canned queries . resource - tuple: (string, string) The name of the database, then the name of the canned query Default allow .","[""Authentication and permissions"", ""Built-in permissions""]","[{""href"": ""https://latest.datasette.io/fixtures/pragma_cache_size"", ""label"": ""https://latest.datasette.io/fixtures/pragma_cache_size""}]" metadata:metadata-column-descriptions,metadata,metadata-column-descriptions,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 .","[""Metadata""]","[{""href"": ""https://latest.datasette.io/fixtures/roadside_attractions"", ""label"": ""latest.datasette.io/fixtures/roadside_attractions""}]" changelog:through-for-joins-through-many-to-many-tables,changelog,through-for-joins-through-many-to-many-tables,?_through= for joins through many-to-many tables,"The new ?_through={json} argument to the Table view allows records to be filtered based on a many-to-many relationship. See Special table arguments for full documentation - here's an example . ( #355 ) This feature was added to help support facet by many-to-many , which isn't quite ready yet but will be coming in the next Datasette release.","[""Changelog"", ""0.29 (2019-07-07)""]","[{""href"": ""https://latest.datasette.io/fixtures/roadside_attractions?_through={%22table%22:%22roadside_attraction_characteristics%22,%22column%22:%22characteristic_id%22,%22value%22:%221%22}"", ""label"": ""an example""}, {""href"": ""https://github.com/simonw/datasette/issues/355"", ""label"": ""#355""}, {""href"": ""https://github.com/simonw/datasette/issues/551"", ""label"": ""facet by many-to-many""}]" full_text_search:full-text-search-table-or-view,full_text_search,full-text-search-table-or-view,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]]]","[""Full-text search""]","[{""href"": ""https://latest.datasette.io/fixtures/searchable_view?_fts_table=searchable_fts&_fts_pk=pk"", ""label"": ""https://latest.datasette.io/fixtures/searchable_view?_fts_table=searchable_fts&_fts_pk=pk""}]" authentication:authentication-permissions-table,authentication,authentication-permissions-table,Access to specific tables and views,"To limit access to the users table in your bakery.db database: [[[cog config_example(cog, """""" databases: bakery: tables: users: allow: id: '*' """""") ]]] [[[end]]] This works for SQL views as well - you can list their names in the ""tables"" block above in the same way as regular tables. Restricting access to tables and views in this way will NOT prevent users from querying them using arbitrary SQL queries, like this for example. If you are restricting access to specific tables you should also use the ""allow_sql"" block to prevent users from bypassing the limit with their own SQL queries - see Controlling the ability to execute arbitrary SQL .","[""Authentication and permissions"", ""Access permissions in ""]","[{""href"": ""https://latest.datasette.io/fixtures?sql=select+*+from+facetable"", ""label"": ""like this""}]" authentication:permissions-execute-sql,authentication,permissions-execute-sql,execute-sql,"Actor is allowed to run arbitrary SQL queries against a specific database, e.g. https://latest.datasette.io/fixtures?sql=select+100 resource - string The name of the database Default allow . See also the default_allow_sql setting .","[""Authentication and permissions"", ""Built-in permissions""]","[{""href"": ""https://latest.datasette.io/fixtures?sql=select+100"", ""label"": ""https://latest.datasette.io/fixtures?sql=select+100""}]" changelog:id13,changelog,id13,0.62 (2022-08-14),"Datasette can now run entirely in your browser using WebAssembly. Try out Datasette Lite , take a look at the code or read more about it in Datasette Lite: a server-side Python web application running in a browser . Datasette now has a Discord community for questions and discussions about Datasette and its ecosystem of projects.","[""Changelog""]","[{""href"": ""https://lite.datasette.io/"", ""label"": ""Datasette Lite""}, {""href"": ""https://github.com/simonw/datasette-lite"", ""label"": ""at the code""}, {""href"": ""https://simonwillison.net/2022/May/4/datasette-lite/"", ""label"": ""Datasette Lite: a server-side Python web application running in a browser""}, {""href"": ""https://datasette.io/discord"", ""label"": ""Discord community""}]" getting_started:getting-started-datasette-lite,getting_started,getting-started-datasette-lite,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 .","[""Getting started""]","[{""href"": ""https://lite.datasette.io/"", ""label"": ""Datasette Lite""}, {""href"": ""https://lite.datasette.io/?url=https%3A%2F%2Fraw.githubusercontent.com%2FNUKnightLab%2Fsql-mysteries%2Fmaster%2Fsql-murder-mystery.db#/sql-murder-mystery"", ""label"": ""example link""}, {""href"": ""https://github.com/NUKnightLab/sql-mysteries"", ""label"": ""Northwestern University Knight Lab""}]" deploying:nginx-proxy-configuration,deploying,nginx-proxy-configuration,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/ .","[""Deploying Datasette"", ""Running Datasette behind a proxy""]","[{""href"": ""https://nginx.org/"", ""label"": ""nginx""}]" contributing:contributing-formatting-prettier,contributing,contributing-formatting-prettier,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","[""Contributing"", ""Code formatting""]","[{""href"": ""https://nodejs.org/en/download/package-manager/"", ""label"": ""install Node.js""}]" changelog:id30,changelog,id30,0.57 (2021-06-05),"This release fixes a reflected cross-site scripting security hole with the ?_trace=1 feature. You should upgrade to this version, or to Datasette 0.56.1, as soon as possible. ( #1360 ) In addition to the security fix, this release includes ?_col= and ?_nocol= options for controlling which columns are displayed for a table, ?_facet_size= for increasing the number of facet results returned, re-display of your SQL query should an error occur and numerous bug fixes.","[""Changelog""]","[{""href"": ""https://owasp.org/www-community/attacks/xss/#reflected-xss-attacks"", ""label"": ""reflected cross-site scripting""}, {""href"": ""https://github.com/simonw/datasette/issues/1360"", ""label"": ""#1360""}]" changelog:id31,changelog,id31,0.56.1 (2021-06-05),"This release fixes a reflected cross-site scripting security hole with the ?_trace=1 feature. You should upgrade to this version, or to Datasette 0.57, as soon as possible. ( #1360 )","[""Changelog""]","[{""href"": ""https://owasp.org/www-community/attacks/xss/#reflected-xss-attacks"", ""label"": ""reflected cross-site scripting""}, {""href"": ""https://github.com/simonw/datasette/issues/1360"", ""label"": ""#1360""}]" metadata:specifying-units-for-a-column,metadata,specifying-units-for-a-column,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]]]","[""Metadata""]","[{""href"": ""https://pint.readthedocs.io/"", ""label"": ""Pint""}, {""href"": ""https://github.com/hgrecco/pint/blob/master/pint/default_en.txt"", ""label"": ""unit registry""}, {""href"": ""http://pint.readthedocs.io/en/latest/defining.html"", ""label"": ""custom units""}]" installation:installation-pipx,installation,installation-pipx,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.","[""Installation"", ""Advanced installation options""]","[{""href"": ""https://pipxproject.github.io/pipx/"", ""label"": ""pipx""}, {""href"": ""https://brew.sh/"", ""label"": ""Homebrew""}]" plugins:id1,plugins,id1,Plugins,"Datasette's plugin system allows additional features to be implemented as Python code (or front-end JavaScript) which can be wrapped up in a separate Python package. The underlying mechanism uses pluggy . See the Datasette plugins directory for a list of existing plugins, or take a look at the datasette-plugin topic on GitHub. Things you can do with plugins include: Add visualizations to Datasette, for example datasette-cluster-map and datasette-vega . Make new custom SQL functions available for use within Datasette, for example datasette-haversine and datasette-jellyfish . Define custom output formats with custom extensions, for example datasette-atom and datasette-ics . Add template functions that can be called within your Jinja custom templates, for example datasette-render-markdown . Customize how database values are rendered in the Datasette interface, for example datasette-render-binary and datasette-pretty-json . Customize how Datasette's authentication and permissions systems work, for example datasette-auth-passwords and datasette-permissions-sql .",[],"[{""href"": ""https://pluggy.readthedocs.io/"", ""label"": ""pluggy""}, {""href"": ""https://datasette.io/plugins"", ""label"": ""Datasette plugins directory""}, {""href"": ""https://github.com/topics/datasette-plugin"", ""label"": ""datasette-plugin""}, {""href"": ""https://github.com/simonw/datasette-cluster-map"", ""label"": ""datasette-cluster-map""}, {""href"": ""https://github.com/simonw/datasette-vega"", ""label"": ""datasette-vega""}, {""href"": ""https://github.com/simonw/datasette-haversine"", ""label"": ""datasette-haversine""}, {""href"": ""https://github.com/simonw/datasette-jellyfish"", ""label"": ""datasette-jellyfish""}, {""href"": ""https://github.com/simonw/datasette-atom"", ""label"": ""datasette-atom""}, {""href"": ""https://github.com/simonw/datasette-ics"", ""label"": ""datasette-ics""}, {""href"": ""https://github.com/simonw/datasette-render-markdown#markdown-in-templates"", ""label"": ""datasette-render-markdown""}, {""href"": ""https://github.com/simonw/datasette-render-binary"", ""label"": ""datasette-render-binary""}, {""href"": ""https://github.com/simonw/datasette-pretty-json"", ""label"": ""datasette-pretty-json""}, {""href"": ""https://github.com/simonw/datasette-auth-passwords"", ""label"": ""datasette-auth-passwords""}, {""href"": ""https://github.com/simonw/datasette-permissions-sql"", ""label"": ""datasette-permissions-sql""}]" plugin_hooks:id1,plugin_hooks,id1,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)",[],"[{""href"": ""https://pluggy.readthedocs.io/"", ""label"": ""pluggy""}]" plugin_hooks:plugin-hook-slots,plugin_hooks,plugin-hook-slots,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 <h1> 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.","[""Plugin hooks""]","[{""href"": ""https://pluggy.readthedocs.io/en/stable/#call-time-order"", ""label"": ""call time order options""}]" changelog:id14,changelog,id14,Features,"Datasette is now compatible with Pyodide . This is the enabling technology behind Datasette Lite . ( #1733 ) Database file downloads now implement conditional GET using ETags. ( #1739 ) HTML for facet results and suggested results has been extracted out into new templates _facet_results.html and _suggested_facets.html . Thanks, M. Nasimul Haque. ( #1759 ) Datasette now runs some SQL queries in parallel. This has limited impact on performance, see this research issue for details. New --nolock option for ignoring file locks when opening read-only databases. ( #1744 ) Spaces in the database names in URLs are now encoded as + rather than ~20 . ( #1701 ) <Binary: 2427344 bytes> is now displayed as <Binary: 2,427,344 bytes> and is accompanied by tooltip showing ""2.3MB"". ( #1712 ) The base Docker image used by datasette publish cloudrun , datasette package and the official Datasette image has been upgraded to 3.10.6-slim-bullseye . ( #1768 ) Canned writable queries against immutable databases now show a warning message. ( #1728 ) datasette publish cloudrun has a new --timeout option which can be used to increase the time limit applied by the Google Cloud build environment. Thanks, Tim Sherratt. ( #1717 ) datasette publish cloudrun has new --min-instances and --max-instances options. ( #1779 )","[""Changelog"", ""0.62 (2022-08-14)""]","[{""href"": ""https://pyodide.org/"", ""label"": ""Pyodide""}, {""href"": ""https://lite.datasette.io/"", ""label"": ""Datasette Lite""}, {""href"": ""https://github.com/simonw/datasette/issues/1733"", ""label"": ""#1733""}, {""href"": ""https://github.com/simonw/datasette/issues/1739"", ""label"": ""#1739""}, {""href"": ""https://github.com/simonw/datasette/pull/1759"", ""label"": ""#1759""}, {""href"": ""https://github.com/simonw/datasette/issues/1727"", ""label"": ""this research issue""}, {""href"": ""https://github.com/simonw/datasette/issues/1744"", ""label"": ""#1744""}, {""href"": ""https://github.com/simonw/datasette/issues/1701"", ""label"": ""#1701""}, {""href"": ""https://github.com/simonw/datasette/issues/1712"", ""label"": ""#1712""}, {""href"": ""https://hub.docker.com/datasetteproject/datasette"", ""label"": ""official Datasette image""}, {""href"": ""https://github.com/simonw/datasette/issues/1768"", ""label"": ""#1768""}, {""href"": ""https://github.com/simonw/datasette/issues/1728"", ""label"": ""#1728""}, {""href"": ""https://github.com/simonw/datasette/pull/1717"", ""label"": ""#1717""}, {""href"": ""https://github.com/simonw/datasette/issues/1779"", ""label"": ""#1779""}]" settings:setting-num-sql-threads,settings,setting-num-sql-threads,num_sql_threads,"Maximum number of threads in the thread pool Datasette uses to execute SQLite queries. Defaults to 3. datasette mydatabase.db --setting num_sql_threads 10 Setting this to 0 turns off threaded SQL queries entirely - useful for environments that do not support threading such as Pyodide .","[""Settings"", ""Settings""]","[{""href"": ""https://pyodide.org/"", ""label"": ""Pyodide""}]" writing_plugins:id1,writing_plugins,id1,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.",[],"[{""href"": ""https://pypi.org/"", ""label"": ""PyPI""}, {""href"": ""https://datasette.io/plugins"", ""label"": ""Datasette plugins directory""}]" spatialite:importing-geojson-polygons-using-shapely,spatialite,importing-geojson-polygons-using-shapely,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()","[""SpatiaLite""]","[{""href"": ""https://pypi.org/project/Shapely/"", ""label"": ""Shapely""}, {""href"": ""https://whosonfirst.org/"", ""label"": ""Who's On First""}, {""href"": ""https://spelunker.whosonfirst.org/id/404227475/"", ""label"": ""spelunker.whosonfirst.org/id/404227475""}, {""href"": ""https://data.whosonfirst.org/404/227/475/404227475.geojson"", ""label"": ""data.whosonfirst.org/404/227/475/404227475.geojson""}]" contributing:contributing-formatting-blacken-docs,contributing,contributing-formatting-blacken-docs,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","[""Contributing"", ""Code formatting""]","[{""href"": ""https://pypi.org/project/blacken-docs/"", ""label"": ""blacken-docs""}]" index:datasette,index,datasette,Datasette,"An open source multi-tool for exploring and publishing data Datasette is a tool for exploring and publishing data. It helps people take data of any shape or size and publish that as an interactive, explorable website and accompanying API. Datasette is aimed at data journalists, museum curators, archivists, local governments and anyone else who has data that they wish to share with the world. It is part of a wider ecosystem of tools and plugins dedicated to making working with structured data as productive as possible. Explore a demo , watch a presentation about the project or Try Datasette without installing anything using Glitch . Interested in learning Datasette? Start with the official tutorials . Support questions, feedback? Join the Datasette Discord .",[],"[{""href"": ""https://pypi.org/project/datasette/"", ""label"": null}, {""href"": ""https://docs.datasette.io/en/stable/changelog.html"", ""label"": null}, {""href"": ""https://pypi.org/project/datasette/"", ""label"": null}, {""href"": ""https://github.com/simonw/datasette/actions?query=workflow%3ATest"", ""label"": null}, {""href"": ""https://github.com/simonw/datasette/blob/main/LICENSE"", ""label"": null}, {""href"": ""https://hub.docker.com/r/datasetteproject/datasette"", ""label"": null}, {""href"": ""https://datasette.io/discord"", ""label"": null}, {""href"": ""https://pypi.org/project/datasette/"", ""label"": null}, {""href"": ""https://docs.datasette.io/en/stable/changelog.html"", ""label"": null}, {""href"": ""https://pypi.org/project/datasette/"", ""label"": null}, {""href"": ""https://github.com/simonw/datasette/actions?query=workflow%3ATest"", ""label"": null}, {""href"": ""https://github.com/simonw/datasette/blob/main/LICENSE"", ""label"": null}, {""href"": ""https://hub.docker.com/r/datasetteproject/datasette"", ""label"": null}, {""href"": ""https://datasette.io/discord"", ""label"": null}, {""href"": ""https://fivethirtyeight.datasettes.com/fivethirtyeight"", ""label"": ""Explore a demo""}, {""href"": ""https://static.simonwillison.net/static/2018/pybay-datasette/"", ""label"": ""a presentation about the project""}, {""href"": ""https://datasette.io/tutorials"", ""label"": ""the official tutorials""}, {""href"": ""https://datasette.io/discord"", ""label"": ""Datasette Discord""}]" testing_plugins:testing-plugins-pytest-httpx,testing_plugins,testing-plugins-pytest-httpx,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( """""" <form action=""/-/fetch-url"" method=""post""> <input type=""hidden"" name=""csrftoken"" value=""{}""> <input name=""url""><input type=""submit""> </form>"""""".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/"" )","[""Testing plugins""]","[{""href"": ""https://pypi.org/project/pytest-httpx/"", ""label"": ""pytest-httpx""}]" contributing:contributing-documentation,contributing,contributing-documentation,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.","[""Contributing""]","[{""href"": ""https://readthedocs.org/"", ""label"": ""Read The Docs""}, {""href"": ""https://simonwillison.net/2018/Aug/25/restructuredtext/"", ""label"": ""The subset of reStructuredText worth committing to memory""}, {""href"": ""https://pypi.org/project/sphinx-autobuild/"", ""label"": ""sphinx-autobuild""}]" pages:tableview,pages,tableview,Table,"The table page is the heart of Datasette: it allows users to interactively explore the contents of a database table, including sorting, filtering, Full-text search and applying Facets . The HTML interface is worth spending some time exploring. As with other pages, you can return the JSON data by appending .json to the URL path, before any ? query string arguments. The query string arguments are described in more detail here: Table arguments You can also use the table page to interactively construct a SQL query - by applying different filters and a sort order for example - and then click the ""View and edit SQL"" link to see the SQL query that was used for the page and edit and re-submit it. Some examples: ../items lists all of the line-items registered by UK MPs as potential conflicts of interest. It demonstrates Datasette's support for Full-text search . ../antiquities-act%2Factions_under_antiquities_act is an interface for exploring the ""actions under the antiquities act"" data table published by FiveThirtyEight. ../global-power-plants?country_long=United+Kingdom&primary_fuel=Gas is a filtered table page showing every Gas power plant in the United Kingdom. It includes some default facets (configured using its metadata.json ) and uses the datasette-cluster-map plugin to show a map of the results.","[""Pages and API endpoints""]","[{""href"": ""https://register-of-members-interests.datasettes.com/regmem/items"", ""label"": ""../items""}, {""href"": ""https://fivethirtyeight.datasettes.com/fivethirtyeight/antiquities-act%2Factions_under_antiquities_act"", ""label"": ""../antiquities-act%2Factions_under_antiquities_act""}, {""href"": ""https://global-power-plants.datasettes.com/global-power-plants/global-power-plants?_facet=primary_fuel&_facet=owner&_facet=country_long&country_long__exact=United+Kingdom&primary_fuel=Gas"", ""label"": ""../global-power-plants?country_long=United+Kingdom&primary_fuel=Gas""}, {""href"": ""https://global-power-plants.datasettes.com/-/metadata"", ""label"": ""its metadata.json""}, {""href"": ""https://github.com/simonw/datasette-cluster-map"", ""label"": ""datasette-cluster-map""}]" pages:rowview,pages,rowview,Row,"Every row in every Datasette table has its own URL. This means individual records can be linked to directly. Table cells with extremely long text contents are truncated on the table view according to the truncate_cells_html setting. If a cell has been truncated the full length version of that cell will be available on the row page. Rows which are the targets of foreign key references from other tables will show a link to a filtered search for all records that reference that row. Here's an example from the Registers of Members Interests database: ../people/uk~2Eorg~2Epublicwhip~2Fperson~2F10001 Note that this URL includes the encoded primary key of the record. Here's that same page as JSON: ../people/uk~2Eorg~2Epublicwhip~2Fperson~2F10001.json","[""Pages and API endpoints""]","[{""href"": ""https://register-of-members-interests.datasettes.com/regmem/people/uk~2Eorg~2Epublicwhip~2Fperson~2F10001"", ""label"": ""../people/uk~2Eorg~2Epublicwhip~2Fperson~2F10001""}, {""href"": ""https://register-of-members-interests.datasettes.com/regmem/people/uk~2Eorg~2Epublicwhip~2Fperson~2F10001.json"", ""label"": ""../people/uk~2Eorg~2Epublicwhip~2Fperson~2F10001.json""}]" json_api:json-api-pagination,json_api,json-api-pagination,Pagination,"The default JSON representation includes a ""next_url"" key which can be used to access the next page of results. If that key is null or missing then it means you have reached the final page of results. Other representations include pagination information in the link HTTP header. That header will look something like this: link: <https://latest.datasette.io/fixtures/sortable.json?_next=d%2Cv>; rel=""next"" Here is an example Python function built using requests that returns a list of all of the paginated items from one of these API endpoints: def paginate(url): items = [] while url: response = requests.get(url) try: url = response.links.get(""next"").get(""url"") except AttributeError: url = None items.extend(response.json()) return items","[""JSON API""]","[{""href"": ""https://requests.readthedocs.io/"", ""label"": ""requests""}]" introspection:jsondataview-plugins,introspection,jsondataview-plugins,/-/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.","[""Introspection""]","[{""href"": ""https://san-francisco.datasettes.com/-/plugins"", ""label"": ""Plugins example""}]" plugin_hooks:plugin-hook-handle-exception,plugin_hooks,plugin-hook-handle-exception,"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","[""Plugin hooks""]","[{""href"": ""https://sentry.io/"", ""label"": ""Sentry""}, {""href"": ""https://datasette.io/plugins/datasette-sentry"", ""label"": ""datasette-sentry""}]" changelog:id118,changelog,id118,0.22 (2018-05-20),"The big new feature in this release is Facets . Datasette can now apply faceted browse to any column in any table. It will also suggest possible facets. See the Datasette Facets announcement post for more details. In addition to the work on facets: Added docs for introspection endpoints New --config option, added --help-config , closes #274 Removed the --page_size= argument to datasette serve in favour of: datasette serve --config default_page_size:50 mydb.db Added new help section: datasette --help-config Config options: 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) 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) Only apply responsive table styles to .rows-and-column Otherwise they interfere with tables in the description, e.g. on https://fivethirtyeight.datasettes.com/fivethirtyeight/nba-elo%2Fnbaallelo Refactored views into new views/ modules, refs #256 Documentation for SQLite full-text search support, closes #253 /-/versions now includes SQLite fts_versions , closes #252","[""Changelog""]","[{""href"": ""https://simonwillison.net/2018/May/20/datasette-facets/"", ""label"": ""Datasette Facets""}, {""href"": ""https://docs.datasette.io/en/stable/introspection.html"", ""label"": ""docs for introspection endpoints""}, {""href"": ""https://github.com/simonw/datasette/issues/274"", ""label"": ""#274""}, {""href"": ""https://fivethirtyeight.datasettes.com/fivethirtyeight/nba-elo%2Fnbaallelo"", ""label"": ""https://fivethirtyeight.datasettes.com/fivethirtyeight/nba-elo%2Fnbaallelo""}, {""href"": ""https://github.com/simonw/datasette/issues/256"", ""label"": ""#256""}, {""href"": ""https://docs.datasette.io/en/stable/full_text_search.html"", ""label"": ""Documentation for SQLite full-text search""}, {""href"": ""https://github.com/simonw/datasette/issues/253"", ""label"": ""#253""}, {""href"": ""https://github.com/simonw/datasette/issues/252"", ""label"": ""#252""}]" changelog:v0-28-databases-that-change,changelog,v0-28-databases-that-change,Supporting databases that change,"From the beginning of the project, Datasette has been designed with read-only databases in mind. If a database is guaranteed not to change it opens up all kinds of interesting opportunities - from taking advantage of SQLite immutable mode and HTTP caching to bundling static copies of the database directly in a Docker container. The interesting ideas in Datasette explores this idea in detail. As my goals for the project have developed, I realized that read-only databases are no longer the right default. SQLite actually supports concurrent access very well provided only one thread attempts to write to a database at a time, and I keep encountering sensible use-cases for running Datasette on top of a database that is processing inserts and updates. So, as-of version 0.28 Datasette no longer assumes that a database file will not change. It is now safe to point Datasette at a SQLite database which is being updated by another process. Making this change was a lot of work - see tracking tickets #418 , #419 and #420 . It required new thinking around how Datasette should calculate table counts (an expensive operation against a large, changing database) and also meant reconsidering the ""content hash"" URLs Datasette has used in the past to optimize the performance of HTTP caches. Datasette can still run against immutable files and gains numerous performance benefits from doing so, but this is no longer the default behaviour. Take a look at the new Performance and caching documentation section for details on how to make the most of Datasette against data that you know will be staying read-only and immutable.","[""Changelog"", ""0.28 (2019-05-19)""]","[{""href"": ""https://simonwillison.net/2018/Oct/4/datasette-ideas/"", ""label"": ""The interesting ideas in Datasette""}, {""href"": ""https://github.com/simonw/datasette/issues/418"", ""label"": ""#418""}, {""href"": ""https://github.com/simonw/datasette/issues/419"", ""label"": ""#419""}, {""href"": ""https://github.com/simonw/datasette/issues/420"", ""label"": ""#420""}]" changelog:id58,changelog,id58,0.45 (2020-07-01),"See also Datasette 0.45: The annotated release notes . Magic parameters for canned queries, a log out feature, improved plugin documentation and four new plugin hooks.","[""Changelog""]","[{""href"": ""https://simonwillison.net/2020/Jul/1/datasette-045/"", ""label"": ""Datasette 0.45: The annotated release notes""}]" changelog:id60,changelog,id60,0.44 (2020-06-11),"See also Datasette 0.44: The annotated release notes . Authentication and permissions, writable canned queries, flash messages, new plugin hooks and more.","[""Changelog""]","[{""href"": ""https://simonwillison.net/2020/Jun/12/annotated-release-notes/"", ""label"": ""Datasette 0.44: The annotated release notes""}]" changelog:id51,changelog,id51,0.49 (2020-09-14),"See also Datasette 0.49: The annotated release notes . Writable canned queries now expose a JSON API, see JSON API for writable canned queries . ( #880 ) New mechanism for defining page templates with custom path parameters - a template file called pages/about/{slug}.html will be used to render any requests to /about/something . See Path parameters for pages . ( #944 ) register_output_renderer() render functions can now return a Response . ( #953 ) New --upgrade option for datasette install . ( #945 ) New datasette --pdb option. ( #962 ) datasette --get exit code now reflects the internal HTTP status code. ( #947 ) New raise_404() template function for returning 404 errors. ( #964 ) datasette publish heroku now deploys using Python 3.8.5 Upgraded CodeMirror to 5.57.0. ( #948 ) Upgraded code style to Black 20.8b1. ( #958 ) Fixed bug where selected facets were not correctly persisted in hidden form fields on the table page. ( #963 ) Renamed the default error template from 500.html to error.html . Custom error pages are now documented, see Custom error pages . ( #965 )","[""Changelog""]","[{""href"": ""https://simonwillison.net/2020/Sep/15/datasette-0-49/"", ""label"": ""Datasette 0.49: The annotated release notes""}, {""href"": ""https://github.com/simonw/datasette/issues/880"", ""label"": ""#880""}, {""href"": ""https://github.com/simonw/datasette/issues/944"", ""label"": ""#944""}, {""href"": ""https://github.com/simonw/datasette/issues/953"", ""label"": ""#953""}, {""href"": ""https://github.com/simonw/datasette/issues/945"", ""label"": ""#945""}, {""href"": ""https://github.com/simonw/datasette/issues/962"", ""label"": ""#962""}, {""href"": ""https://github.com/simonw/datasette/issues/947"", ""label"": ""#947""}, {""href"": ""https://github.com/simonw/datasette/issues/964"", ""label"": ""#964""}, {""href"": ""https://codemirror.net/"", ""label"": ""CodeMirror""}, {""href"": ""https://github.com/simonw/datasette/issues/948"", ""label"": ""#948""}, {""href"": ""https://github.com/simonw/datasette/issues/958"", ""label"": ""#958""}, {""href"": ""https://github.com/simonw/datasette/issues/963"", ""label"": ""#963""}, {""href"": ""https://github.com/simonw/datasette/issues/965"", ""label"": ""#965""}]" internals:internals-utils-await-me-maybe,internals,internals-utils-await-me-maybe,await_me_maybe(value),"Utility function for calling await on a return value if it is awaitable, otherwise returning the value. This is used by Datasette to support plugin hooks that can optionally return awaitable functions. Read more about this function in The β€œawait me maybe” pattern for Python asyncio . async datasette.utils. await_me_maybe value : Any Any If value is callable, call it. If awaitable, await it. Otherwise return it.","[""Internals for plugins"", ""The datasette.utils module""]","[{""href"": ""https://simonwillison.net/2020/Sep/2/await-me-maybe/"", ""label"": ""The β€œawait me maybe” pattern for Python asyncio""}]" changelog:id35,changelog,id35,0.54 (2021-01-25),"The two big new features in this release are the _internal SQLite in-memory database storing details of all connected databases and tables, and support for JavaScript modules in plugins and additional scripts. For additional commentary on this release, see Datasette 0.54, the annotated release notes .","[""Changelog""]","[{""href"": ""https://simonwillison.net/2021/Jan/25/datasette/"", ""label"": ""Datasette 0.54, the annotated release notes""}]" changelog:v1-0-a2,changelog,v1-0-a2,1.0a2 (2022-12-14),"The third Datasette 1.0 alpha release adds upsert support to the JSON API, plus the ability to specify finely grained permissions when creating an API token. See Datasette 1.0a2: Upserts and finely grained permissions for an extended, annotated version of these release notes. New /db/table/-/upsert API, documented here . upsert is an update-or-insert: existing rows will have specified keys updated, but if no row matches the incoming primary key a brand new row will be inserted instead. ( #1878 ) New register_permissions(datasette) plugin hook. Plugins can now register named permissions, which will then be listed in various interfaces that show available permissions. ( #1940 ) The /db/-/create API for creating a table now accepts ""ignore"": true and ""replace"": true options when called with the ""rows"" property that creates a new table based on an example set of rows. This means the API can be called multiple times with different rows, setting rules for what should happen if a primary key collides with an existing row. ( #1927 ) Arbitrary permissions can now be configured at the instance, database and resource (table, SQL view or canned query) level in Datasette's Metadata JSON and YAML files. The new ""permissions"" key can be used to specify which actors should have which permissions. See Other permissions in datasette.yaml for details. ( #1636 ) The /-/create-token page can now be used to create API tokens which are restricted to just a subset of actions, including against specific databases or resources. See API Tokens for details. ( #1947 ) Likewise, the datasette create-token CLI command can now create tokens with a subset of permissions . ( #1855 ) New datasette.create_token() API method for programmatically creating signed API tokens. ( #1951 ) /db/-/create API now requires actor to have insert-row permission in order to use the ""row"" or ""rows"" properties. ( #1937 )","[""Changelog""]","[{""href"": ""https://simonwillison.net/2022/Dec/15/datasette-1a2/"", ""label"": ""Datasette 1.0a2: Upserts and finely grained permissions""}, {""href"": ""https://github.com/simonw/datasette/issues/1878"", ""label"": ""#1878""}, {""href"": ""https://github.com/simonw/datasette/issues/1940"", ""label"": ""#1940""}, {""href"": ""https://github.com/simonw/datasette/issues/1927"", ""label"": ""#1927""}, {""href"": ""https://github.com/simonw/datasette/issues/1636"", ""label"": ""#1636""}, {""href"": ""https://github.com/simonw/datasette/issues/1947"", ""label"": ""#1947""}, {""href"": ""https://github.com/simonw/datasette/issues/1855"", ""label"": ""#1855""}, {""href"": ""https://github.com/simonw/datasette/issues/1951"", ""label"": ""#1951""}, {""href"": ""https://github.com/simonw/datasette/issues/1937"", ""label"": ""#1937""}]" changelog:id11,changelog,id11,0.63 (2022-10-27),See Datasette 0.63: The annotated release notes for more background on the changes in this release.,"[""Changelog""]","[{""href"": ""https://simonwillison.net/2022/Oct/27/datasette-0-63/"", ""label"": ""Datasette 0.63: The annotated release notes""}]" changelog:v1-0-a8,changelog,v1-0-a8,1.0a8 (2024-02-07),"This alpha release continues the migration of Datasette's configuration from metadata.yaml to the new datasette.yaml configuration file, introduces a new system for JavaScript plugins and adds several new plugin hooks. See Datasette 1.0a8: JavaScript plugins, new plugin hooks and plugin configuration in datasette.yaml for an annotated version of these release notes.","[""Changelog""]","[{""href"": ""https://simonwillison.net/2024/Feb/7/datasette-1a8/"", ""label"": ""Datasette 1.0a8: JavaScript plugins, new plugin hooks and plugin configuration in datasette.yaml""}]" full_text_search:configuring-fts-using-sqlite-utils,full_text_search,configuring-fts-using-sqlite-utils,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","[""Full-text search"", ""Enabling full-text search for a SQLite table""]","[{""href"": ""https://sqlite-utils.datasette.io/"", ""label"": ""sqlite-utils""}, {""href"": ""https://sqlite-utils.datasette.io/en/latest/python-api.html#enabling-full-text-search"", ""label"": ""it from Python code""}, {""href"": ""https://sqlite-utils.datasette.io/en/latest/cli.html#configuring-full-text-search"", ""label"": ""using the accompanying command-line tool""}]" sql_queries:sql-views,sql_queries,sql-views,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; <CTRL+D> 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""","[""Running SQL queries""]","[{""href"": ""https://sqlite-utils.datasette.io/"", ""label"": ""sqlite-utils""}, {""href"": ""https://sqlite-utils.datasette.io/en/stable/cli.html#creating-views"", ""label"": ""create a view""}]" full_text_search:configuring-fts-by-hand,full_text_search,configuring-fts-by-hand,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.","[""Full-text search"", ""Enabling full-text search for a SQLite table""]","[{""href"": ""https://sqlite-utils.datasette.io/"", ""label"": ""sqlite-utils""}]" ecosystem:sqlite-utils,ecosystem,sqlite-utils,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.","[""The Datasette Ecosystem""]","[{""href"": ""https://sqlite-utils.datasette.io/"", ""label"": ""sqlite-utils""}]" facets:speeding-up-facets-with-indexes,facets,speeding-up-facets-with-indexes,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","[""Facets""]","[{""href"": ""https://sqlite-utils.datasette.io/en/stable/cli.html#creating-indexes"", ""label"": ""sqlite-utils""}]" spatialite:making-use-of-a-spatial-index,spatialite,making-use-of-a-spatial-index,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.","[""SpatiaLite""]","[{""href"": ""https://timezones-api.datasette.io/timezones/idx_timezones_Geometry"", ""label"": ""timezones-api.datasette.io/timezones/idx_timezones_Geometry""}]" changelog:new-visual-design,changelog,new-visual-design,New visual design,"Datasette is no longer white and grey with blue and purple links! Natalie Downe has been working on a visual refresh, the first iteration of which is included in this release. ( #1056 )","[""Changelog"", ""0.51 (2020-10-31)""]","[{""href"": ""https://twitter.com/natbat"", ""label"": ""Natalie Downe""}, {""href"": ""https://github.com/simonw/datasette/pull/1056"", ""label"": ""#1056""}]" performance:http-caching,performance,http-caching,HTTP caching,"If your database is immutable and guaranteed not to change, you can gain major performance improvements from Datasette by enabling HTTP caching. This can work at two different levels. First, it can tell browsers to cache the results of queries and serve future requests from the browser cache. More significantly, it allows you to run Datasette behind a caching proxy such as Varnish or use a cache provided by a hosted service such as Fastly or Cloudflare . This can provide incredible speed-ups since a query only needs to be executed by Datasette the first time it is accessed - all subsequent hits can then be served by the cache. Using a caching proxy in this way could enable a Datasette-backed visualization to serve thousands of hits a second while running Datasette itself on extremely inexpensive hosting. Datasette's integration with HTTP caches can be enabled using a combination of configuration options and query string arguments. The default_cache_ttl setting sets the default HTTP cache TTL for all Datasette pages. This is 5 seconds unless you change it - you can set it to 0 if you wish to disable HTTP caching entirely. You can also change the cache timeout on a per-request basis using the ?_ttl=10 query string parameter. This can be useful when you are working with the Datasette JSON API - you may decide that a specific query can be cached for a longer time, or maybe you need to set ?_ttl=0 for some requests for example if you are running a SQL order by random() query.","[""Performance and caching""]","[{""href"": ""https://varnish-cache.org/"", ""label"": ""Varnish""}, {""href"": ""https://www.fastly.com/"", ""label"": ""Fastly""}, {""href"": ""https://www.cloudflare.com/"", ""label"": ""Cloudflare""}]" publish:publish-vercel,publish,publish-vercel,Publishing to Vercel,"Vercel - previously known as Zeit Now - provides a layer over AWS Lambda to allow for quick, scale-to-zero deployment. You can deploy Datasette instances to Vercel using the datasette-publish-vercel plugin. pip install datasette-publish-vercel datasette publish vercel mydatabase.db --project my-database-project Not every feature is supported: consult the datasette-publish-vercel README for more details.","[""Publishing data"", ""datasette publish""]","[{""href"": ""https://vercel.com/"", ""label"": ""Vercel""}, {""href"": ""https://github.com/simonw/datasette-publish-vercel"", ""label"": ""datasette-publish-vercel""}, {""href"": ""https://github.com/simonw/datasette-publish-vercel/blob/main/README.md"", ""label"": ""datasette-publish-vercel README""}]" deploying:deploying-openrc,deploying,deploying-openrc,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","[""Deploying Datasette""]","[{""href"": ""https://www.alpinelinux.org/"", ""label"": ""Alpine Linux""}, {""href"": ""https://www.gentoo.org/"", ""label"": ""Gentoo""}]" publish:cli-package,publish,cli-package,datasette package,"If you have docker installed (e.g. using Docker for Mac ) you can use the datasette package command to create a new Docker image in your local repository containing the datasette app bundled together with one or more SQLite databases: datasette package mydatabase.db Here's example output for the package command: datasette package parlgov.db --extra-options=""--setting sql_time_limit_ms 2500"" Sending build context to Docker daemon 4.459MB Step 1/7 : FROM python:3.11.0-slim-bullseye ---> 79e1dc9af1c1 Step 2/7 : COPY . /app ---> Using cache ---> cd4ec67de656 Step 3/7 : WORKDIR /app ---> Using cache ---> 139699e91621 Step 4/7 : RUN pip install datasette ---> Using cache ---> 340efa82bfd7 Step 5/7 : RUN datasette inspect parlgov.db --inspect-file inspect-data.json ---> Using cache ---> 5fddbe990314 Step 6/7 : EXPOSE 8001 ---> Using cache ---> 8e83844b0fed Step 7/7 : CMD datasette serve parlgov.db --port 8001 --inspect-file inspect-data.json --setting sql_time_limit_ms 2500 ---> Using cache ---> 1bd380ea8af3 Successfully built 1bd380ea8af3 You can now run the resulting container like so: docker run -p 8081:8001 1bd380ea8af3 This exposes port 8001 inside the container as port 8081 on your host machine, so you can access the application at http://localhost:8081/ You can customize the port that is exposed by the container using the --port option: datasette package mydatabase.db --port 8080 A full list of options can be seen by running datasette package --help : See datasette package for the full list of options for this command.","[""Publishing data""]","[{""href"": ""https://www.docker.com/docker-mac"", ""label"": ""Docker for Mac""}]" spatialite:id1,spatialite,id1,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",[],"[{""href"": ""https://www.gaia-gis.it/fossil/libspatialite/index"", ""label"": ""SpatiaLite module""}, {""href"": ""https://datasette.io/tutorials/spatialite"", ""label"": ""Building a location to time zone API with SpatiaLite""}]" changelog:improved-support-for-spatialite,changelog,improved-support-for-spatialite,Improved support for SpatiaLite,"The SpatiaLite module for SQLite adds robust geospatial features to the database. Getting SpatiaLite working can be tricky, especially if you want to use the most recent alpha version (with support for K-nearest neighbor). Datasette now includes extensive documentation on SpatiaLite , and thanks to Ravi Kotecha our GitHub repo includes a Dockerfile that can build the latest SpatiaLite and configure it for use with Datasette. The datasette publish and datasette package commands now accept a new --spatialite argument which causes them to install and configure SpatiaLite as part of the container they deploy.","[""Changelog"", ""0.23 (2018-06-18)""]","[{""href"": ""https://www.gaia-gis.it/fossil/libspatialite/index"", ""label"": ""SpatiaLite module""}, {""href"": ""https://github.com/r4vi"", ""label"": ""Ravi Kotecha""}, {""href"": ""https://github.com/simonw/datasette/blob/master/Dockerfile"", ""label"": ""Dockerfile""}]" spatialite:spatialite-warning,spatialite,spatialite-warning,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","[""SpatiaLite""]","[{""href"": ""https://www.gaia-gis.it/gaia-sins/spatialite-sql-5.0.1.html"", ""label"": ""a large number of additional SQL functions""}, {""href"": ""https://datasette.io/tutorials/spatialite"", ""label"": ""Datasette SpatiaLite tutorial""}]" publish:publish-heroku,publish,publish-heroku,Publishing to Heroku,"To publish your data using Heroku , first create an account there and install and configure the Heroku CLI tool . You can publish one or more databases to Heroku using the following command: datasette publish heroku mydatabase.db This will output some details about the new deployment, including a URL like this one: https://limitless-reef-88278.herokuapp.com/ deployed to Heroku You can specify a custom app name by passing -n my-app-name to the publish command. This will also allow you to overwrite an existing app. Rather than deploying directly you can use the --generate-dir option to output the files that would be deployed to a directory: datasette publish heroku mydatabase.db --generate-dir=/tmp/deploy-this-to-heroku See datasette publish heroku for the full list of options for this command.","[""Publishing data"", ""datasette publish""]","[{""href"": ""https://www.heroku.com/"", ""label"": ""Heroku""}, {""href"": ""https://devcenter.heroku.com/articles/heroku-cli"", ""label"": ""Heroku CLI tool""}]" deploying:deploying-buildpacks,deploying,deploying-buildpacks,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.","[""Deploying Datasette""]","[{""href"": ""https://www.heroku.com/"", ""label"": ""Heroku""}, {""href"": ""https://www.digitalocean.com/docs/app-platform/"", ""label"": ""DigitalOcean App Platform""}, {""href"": ""https://scalingo.com/"", ""label"": ""Scalingo""}, {""href"": ""https://buildpacks.io/"", ""label"": ""Buildpacks standard""}, {""href"": ""https://github.com/simonw/buildpack-datasette-demo"", ""label"": ""simonw/buildpack-datasette-demo""}]" internals:internals-datasette-client,internals,internals-datasette-client,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 .","[""Internals for plugins"", ""Datasette class""]","[{""href"": ""https://www.python-httpx.org/"", ""label"": ""HTTPX Python library""}, {""href"": ""https://requests.readthedocs.io/"", ""label"": ""Requests library""}, {""href"": ""https://www.python-httpx.org/async/"", ""label"": ""HTTPX Async documentation""}]" testing_plugins:testing-datasette-client,testing_plugins,testing-datasette-client,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 ""<h1>"" 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""}}","[""Testing plugins""]","[{""href"": ""https://www.python-httpx.org/async/"", ""label"": ""HTTPX async client""}]" installation:installation-pip,installation,installation-pip,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","[""Installation"", ""Basic installation""]","[{""href"": ""https://www.python.org/about/gettingstarted/"", ""label"": ""Python.org Python For Beginners""}]" full_text_search:id1,full_text_search,id1,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.",[],"[{""href"": ""https://www.sqlite.org/fts3.html"", ""label"": ""a powerful mechanism for enabling full-text search""}, {""href"": ""https://register-of-members-interests.datasettes.com/regmem/items?_search=hamper&_sort_desc=date"", ""label"": ""an example search""}]" json_api:json-api-table-arguments,json_api,json-api-table-arguments,Special table arguments,"?_col=COLUMN1&_col=COLUMN2 List specific columns to display. These will be shown along with any primary keys. ?_nocol=COLUMN1&_nocol=COLUMN2 List specific columns to hide - any column not listed will be displayed. Primary keys cannot be hidden. ?_labels=on/off Expand foreign key references for every possible column. See below. ?_label=COLUMN1&_label=COLUMN2 Expand foreign key references for one or more specified columns. ?_size=1000 or ?_size=max Sets a custom page size. This cannot exceed the max_returned_rows limit passed to datasette serve . Use max to get max_returned_rows . ?_sort=COLUMN Sorts the results by the specified column. ?_sort_desc=COLUMN Sorts the results by the specified column in descending order. ?_search=keywords For SQLite tables that have been configured for full-text search executes a search with the provided keywords. ?_search_COLUMN=keywords Like _search= but allows you to specify the column to be searched, as opposed to searching all columns that have been indexed by FTS. ?_searchmode=raw With this option, queries passed to ?_search= or ?_search_COLUMN= will not have special characters escaped. This means you can make use of the full set of advanced SQLite FTS syntax , though this could potentially result in errors if the wrong syntax is used. ?_where=SQL-fragment If the execute-sql permission is enabled, this parameter can be used to pass one or more additional SQL fragments to be used in the WHERE clause of the SQL used to query the table. This is particularly useful if you are building a JavaScript application that needs to do something creative but still wants the other conveniences provided by the table view (such as faceting) and hence would like not to have to construct a completely custom SQL query. Some examples: facetable?_where=_neighborhood like ""%c%""&_where=_city_id=3 facetable?_where=_city_id in (select id from facet_cities where name != ""Detroit"") ?_through={json} This can be used to filter rows via a join against another table. The JSON parameter must include three keys: table , column and value . table must be a table that the current table is related to via a foreign key relationship. column must be a column in that other table. value is the value that you want to match against. For example, to filter roadside_attractions to just show the attractions that have a characteristic of ""museum"", you would construct this JSON: { ""table"": ""roadside_attraction_characteristics"", ""column"": ""characteristic_id"", ""value"": ""1"" } As a URL, that looks like this: ?_through={%22table%22:%22roadside_attraction_characteristics%22,%22column%22:%22characteristic_id%22,%22value%22:%221%22} Here's an example . ?_next=TOKEN Pagination by continuation token - pass the token that was returned in the ""next"" property by the previous page. ?_facet=column Facet by column. Can be applied multiple times, see Facets . Only works on the default JSON output, not on any of the custom shapes. ?_facet_size=100 Increase the number of facet results returned for each facet. Use ?_facet_size=max for the maximum available size, determined by max_returned_rows . ?_nofacet=1 Disable all facets and facet suggestions for this page, including any defined by Facets in metadata . ?_nosuggest=1 Disable facet suggestions for this page. ?_nocount=1 Disable the select count(*) query used on this page - a count of None will be returned instead.","[""JSON API"", ""Table arguments""]","[{""href"": ""https://www.sqlite.org/fts3.html"", ""label"": ""full-text search""}, {""href"": ""https://www.sqlite.org/fts5.html#full_text_query_syntax"", ""label"": ""advanced SQLite FTS syntax""}, {""href"": ""https://latest.datasette.io/fixtures/facetable?_where=_neighborhood%20like%20%22%c%%22&_where=_city_id=3"", ""label"": ""facetable?_where=_neighborhood like \""%c%\""&_where=_city_id=3""}, {""href"": ""https://latest.datasette.io/fixtures/facetable?_where=_city_id%20in%20(select%20id%20from%20facet_cities%20where%20name%20!=%20%22Detroit%22)"", ""label"": ""facetable?_where=_city_id in (select id from facet_cities where name != \""Detroit\"")""}, {""href"": ""https://latest.datasette.io/fixtures/roadside_attractions?_through={%22table%22:%22roadside_attraction_characteristics%22,%22column%22:%22characteristic_id%22,%22value%22:%221%22}"", ""label"": ""an example""}]" full_text_search:full-text-search-enabling,full_text_search,full-text-search-enabling,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","[""Full-text search""]","[{""href"": ""https://www.sqlite.org/fts3.html#_external_content_fts4_tables_"", ""label"": ""external content""}]" full_text_search:full-text-search-advanced-queries,full_text_search,full-text-search-advanced-queries,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.","[""Full-text search""]","[{""href"": ""https://www.sqlite.org/fts5.html#full_text_query_syntax"", ""label"": ""a variety of advanced queries""}]" settings:setting-cache-size-kb,settings,setting-cache-size-kb,cache_size_kb,"Sets the amount of memory SQLite uses for its per-connection cache , in KB. datasette mydatabase.db --setting cache_size_kb 5000","[""Settings"", ""Settings""]","[{""href"": ""https://www.sqlite.org/pragma.html#pragma_cache_size"", ""label"": ""per-connection cache""}]" changelog:new-configuration-settings,changelog,new-configuration-settings,New configuration settings,"Datasette's Settings now also supports boolean settings. A number of new configuration options have been added: num_sql_threads - the number of threads used to execute SQLite queries. Defaults to 3. allow_facet - enable or disable custom Facets using the _facet= parameter. Defaults to on. suggest_facets - should Datasette suggest facets? Defaults to on. allow_download - should users be allowed to download the entire SQLite database? Defaults to on. allow_sql - should users be allowed to execute custom SQL queries? Defaults to on. default_cache_ttl - Default HTTP caching max-age header in seconds. Defaults to 365 days - caching can be disabled entirely by settings this to 0. cache_size_kb - Set the amount of memory SQLite uses for its per-connection cache , in KB. allow_csv_stream - allow users to stream entire result sets as a single CSV file. Defaults to on. max_csv_mb - maximum size of a returned CSV file in MB. Defaults to 100MB, set to 0 to disable this limit.","[""Changelog"", ""0.23 (2018-06-18)""]","[{""href"": ""https://www.sqlite.org/pragma.html#pragma_cache_size"", ""label"": ""per-connection cache""}]" plugin_hooks:plugin-hook-extra-css-urls,plugin_hooks,plugin-hook-extra-css-urls,"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","[""Plugin hooks"", ""Page extras""]","[{""href"": ""https://www.srihash.org/"", ""label"": ""SRI hash""}, {""href"": ""https://datasette.io/plugins/datasette-cluster-map"", ""label"": ""datasette-cluster-map""}, {""href"": ""https://datasette.io/plugins/datasette-vega"", ""label"": ""datasette-vega""}]" configuration:configuration-reference-css-js,configuration,configuration-reference-css-js,Custom CSS and JavaScript,"Datasette can load additional CSS and JavaScript files, configured in datasette.yaml like this: [[[cog from metadata_doc import config_example config_example(cog, """""" extra_css_urls: - https://simonwillison.net/static/css/all.bf8cd891642c.css extra_js_urls: - https://code.jquery.com/jquery-3.2.1.slim.min.js """""") ]]] [[[end]]] The extra CSS and JavaScript files will be linked in the <head> of every page: <link rel=""stylesheet"" href=""https://simonwillison.net/static/css/all.bf8cd891642c.css""> <script src=""https://code.jquery.com/jquery-3.2.1.slim.min.js""></script> You can also specify a SRI (subresource integrity hash) for these assets: [[[cog config_example(cog, """""" extra_css_urls: - url: https://simonwillison.net/static/css/all.bf8cd891642c.css sri: sha384-9qIZekWUyjCyDIf2YK1FRoKiPJq4PHt6tp/ulnuuyRBvazd0hG7pWbE99zvwSznI extra_js_urls: - url: https://code.jquery.com/jquery-3.2.1.slim.min.js sri: sha256-k2WSCIexGzOj3Euiig+TlR8gA0EmPjuc79OEeY5L45g= """""") ]]] [[[end]]] This will produce: <link rel=""stylesheet"" href=""https://simonwillison.net/static/css/all.bf8cd891642c.css"" integrity=""sha384-9qIZekWUyjCyDIf2YK1FRoKiPJq4PHt6tp/ulnuuyRBvazd0hG7pWbE99zvwSznI"" crossorigin=""anonymous""> <script src=""https://code.jquery.com/jquery-3.2.1.slim.min.js"" integrity=""sha256-k2WSCIexGzOj3Euiig+TlR8gA0EmPjuc79OEeY5L45g="" crossorigin=""anonymous""></script> Modern browsers will only execute the stylesheet or JavaScript if the SRI hash matches the content served. You can generate hashes using www.srihash.org Items in ""extra_js_urls"" can specify ""module"": true if they reference JavaScript that uses JavaScript modules . This configuration: [[[cog config_example(cog, """""" extra_js_urls: - url: https://example.datasette.io/module.js module: true """""") ]]] [[[end]]] Will produce this HTML: <script type=""module"" src=""https://example.datasette.io/module.js""></script>","[""Configuration"", null]","[{""href"": ""https://www.srihash.org/"", ""label"": ""www.srihash.org""}, {""href"": ""https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules"", ""label"": ""JavaScript modules""}]" changelog:id82,changelog,id82,0.29.2 (2019-07-13),"Bumped Uvicorn to 0.8.4, fixing a bug where the query string was not included in the server logs. ( #559 ) Fixed bug where the navigation breadcrumbs were not displayed correctly on the page for a custom query. ( #558 ) Fixed bug where custom query names containing unicode characters caused errors.","[""Changelog""]","[{""href"": ""https://www.uvicorn.org/"", ""label"": ""Uvicorn""}, {""href"": ""https://github.com/simonw/datasette/issues/559"", ""label"": ""#559""}, {""href"": ""https://github.com/simonw/datasette/issues/558"", ""label"": ""#558""}]"