id,page,ref,title,content,breadcrumbs,references changelog:id66,changelog,id66,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:id67,changelog,id67,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""}]" changelog:id68,changelog,id68,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:id69,changelog,id69,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:id7,changelog,id7,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:id70,changelog,id70,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:id71,changelog,id71,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:id72,changelog,id72,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:id73,changelog,id73,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.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:id75,changelog,id75,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:id76,changelog,id76,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""}]" changelog:id77,changelog,id77,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:id78,changelog,id78,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""}]" changelog:id79,changelog,id79,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:id8,changelog,id8,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:id80,changelog,id80,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:id81,changelog,id81,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:id82,changelog,id82,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:id83,changelog,id83,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:id84,changelog,id84,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""}]" changelog:id85,changelog,id85,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:id86,changelog,id86,0.29 (2019-07-07),"ASGI, new plugin hooks, facet by date and much, much more...","[""Changelog""]",[] changelog:id87,changelog,id87,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:id88,changelog,id88,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:id89,changelog,id89,0.27.1 (2019-05-09),"Tiny bugfix release: don't install tests/ in the wrong place. Thanks, Veit Heller.","[""Changelog""]",[] changelog:id9,changelog,id9,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 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""}]" 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: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-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: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""}]" 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-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-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-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""}]" 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-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""]",[] 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""}]" 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""}]" 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""}]" 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""}]" 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""}]" internals:internals-utils-named-parameters,internals,internals-utils-named-parameters,named_parameters(sql),"Derive the list of :named parameters referenced in a SQL query. datasette.utils. named_parameters 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-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""]",[] 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: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""]",[] 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