<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: datasette-desktop</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/datasette-desktop.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2022-07-20T23:13:01+00:00</updated><author><name>Simon Willison</name></author><entry><title>Weeknotes: Datasette, sqlite-utils, Datasette Desktop</title><link href="https://simonwillison.net/2022/Jul/20/weeknotes/#atom-tag" rel="alternate"/><published>2022-07-20T23:13:01+00:00</published><updated>2022-07-20T23:13:01+00:00</updated><id>https://simonwillison.net/2022/Jul/20/weeknotes/#atom-tag</id><summary type="html">
    &lt;p&gt;A flurry of releases this week, including a new Datasette alpha and a fixed Datasette Desktop.&lt;/p&gt;
&lt;h4&gt;datasette 0.62a1&lt;/h4&gt;
&lt;p&gt;Work on Datasette Cloud continues - the last 10% of the work needed for the beta launch is trending towards taking 90% of the time. It's been driving all sorts of fixes to the wider Datasette ecosystem, which is nice.&lt;/p&gt;
&lt;p&gt;I ran into a bug which would have been easier to investigate using &lt;a href="https://sentry.io/"&gt;Sentry&lt;/a&gt;. The &lt;a href="https://datasette.io/plugins/datasette-sentry"&gt;datasette-sentry&lt;/a&gt; plugin wasn't working right, and it turned out I &lt;a href="https://github.com/simonw/datasette/issues/1770"&gt;needed a new handle_exception() plugin hook&lt;/a&gt; to fix it. This was the impetus I needed to push out &lt;a href="https://github.com/simonw/datasette/releases/tag/0.62a1"&gt;a new Datasette alpha&lt;/a&gt; - I like to get new hooks into an alpha as quickly as possible so I can depend on that Datasette version from alpha releases of plugins.&lt;/p&gt;
&lt;p&gt;Here are some other highlights from the alpha's release notes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://docs.datasette.io/en/latest/plugin_hooks.html#plugin-hook-render-cell"&gt;render_cell()&lt;/a&gt; plugin hook is now also passed a &lt;code&gt;row&lt;/code&gt; argument, representing the &lt;code&gt;sqlite3.Row&lt;/code&gt; object that is being rendered. (&lt;a href="https://github.com/simonw/datasette/issues/1300"&gt;#1300&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;A neat thing about &lt;a href="https://pluggy.readthedocs.io/"&gt;Pluggy&lt;/a&gt; is that you can add new arguments to existing plugin hooks without breaking plugins that already use them.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;New &lt;code&gt;--nolock&lt;/code&gt; option for ignoring file locks when opening read-only databases. (&lt;a href="https://github.com/simonw/datasette/issues/1744"&gt;#1744&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Since the very start of the project Datasette has suggested trying the following command to start exploring your Google Chrome history, which is stored using SQLite:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette ~/Library/Application\ Support/Google/Chrome/Default/History
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I'm not sure when this changed, but I tried running the command recently and got the following error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite3.OperationalError: database is locked
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since Datasette opens databases in read-only mode I didn't see why a lock like this should be respected. It turns out SQLite can be told to ignore locks like so:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;sqlite3&lt;/span&gt;.&lt;span class="pl-en"&gt;connect&lt;/span&gt;(
    &lt;span class="pl-s"&gt;"file:places.sqlite?mode=ro&amp;amp;nolock=1"&lt;/span&gt;,
    &lt;span class="pl-s1"&gt;uri&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;
)&lt;/pre&gt;
&lt;p&gt;So I added a &lt;code&gt;--nolock&lt;/code&gt; option to Datasette which does exactly that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette ~/Library/Application\ Support/Google/Chrome/Default/History --nolock
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Datasette now has a &lt;a href="https://discord.gg/ktd74dm5mw"&gt;Discord community&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Inspired by &lt;a href="https://dev.to/fredkschott/5-more-things-i-learned-building-snowpack-to-20-000-stars-5dc9"&gt;6 More Things I Learned Building Snowpack to 20,000 Stars (Part 2)&lt;/a&gt; by Fred K. Schott I finally setup a chat community for Datasette, using &lt;a href="https://discord.com/"&gt;Discord&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It's attracted 88 members already! You can &lt;a href="https://discord.gg/ktd74dm5mw"&gt;join it here&lt;/a&gt;. I wrote detailed notes on how I configured it in &lt;a href="https://github.com/simonw/datasette.io/issues/112"&gt;this issue&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Database file downloads now implement conditional GET using ETags. (&lt;a href="https://github.com/simonw/datasette/issues/1739"&gt;#1739&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is a change I made to support &lt;a href="https://simonwillison.net/2022/May/4/datasette-lite/"&gt;Datasette Lite&lt;/a&gt; - I noticed that the WASM version of Datasette was downloading a fresh database every time, so I added &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#caching_of_unchanged_resources"&gt;ETag support&lt;/a&gt; to encourage browsers to avoid a duplicate download and use a cached copy of the database file instead, provided it hasn't changed.&lt;/p&gt;
&lt;h4&gt;Datasette Desktop&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://datasette.io/desktop"&gt;Datasette Desktop&lt;/a&gt; was hanging on launch. Paul Everitt &lt;a href="https://github.com/simonw/datasette-app/issues/139"&gt;figured out a fix&lt;/a&gt;, which it took me way too long to get around to applying.&lt;/p&gt;
&lt;p&gt;I finally shipped that in &lt;a href="https://github.com/simonw/datasette-app/releases/tag/0.2.2"&gt;Datasette Desktop 0.2.2&lt;/a&gt;, but I wanted to reduce the chances of this happening again as much as possible. Datasette Desktop's Elecron tests used the &lt;a href="https://github.com/electron-userland/spectron"&gt;spectron&lt;/a&gt; test harness, but that's marked as deprecated.&lt;/p&gt;
&lt;p&gt;I'm a big &lt;a href="https://simonwillison.net/tags/playwright/"&gt;fan of Playwright&lt;/a&gt; and I was optimistic to see that it has support for testing Electron apps. I figured out how to use that with Datasette Desktop and run the tests in GitHub Actions: I wrote up what I learned in a TIL, &lt;a href="https://til.simonwillison.net/electron/testing-electron-playwright"&gt;Testing Electron apps with Playwright and GitHub Actions&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;sqlite-utils 3.28&lt;/h4&gt;
&lt;p&gt;Annotated &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-28"&gt;release notes&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;New &lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html#python-api-duplicate"&gt;table.duplicate(new_name)&lt;/a&gt; method for creating a copy of a table with a matching schema and row contents. Thanks, &lt;a href="https://github.com/davidleejy"&gt;David&lt;/a&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/449"&gt;#449&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;New &lt;code&gt;sqlite-utils duplicate data.db table_name new_name&lt;/code&gt; CLI command for &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-duplicate-table"&gt;Duplicating tables&lt;/a&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/454"&gt;#454&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/davidleejy"&gt;davidleejy&lt;/a&gt; suggested the &lt;code&gt;table.duplicate()&lt;/code&gt; method and contributed an implementation. This was the impetus for pushing out a fresh release.&lt;/p&gt;
&lt;p&gt;I added the CLI equivalent, &lt;code&gt;sqlite-utils duplicate&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sqlite_utils.utils.rows_from_file()&lt;/code&gt; is now a &lt;a href="https://sqlite-utils.datasette.io/en/stable/reference.html#reference-utils-rows-from-file"&gt;documented API&lt;/a&gt;. It can be used to read a sequence of dictionaries from a file-like object containing CSV, TSV, JSON or newline-delimited JSON. It can be passed an explicit format or can attempt to detect the format automatically. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/443"&gt;#443&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sqlite_utils.utils.TypeTracker&lt;/code&gt; is now a documented API for detecting the likely column types for a sequence of string rows, see &lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html#python-api-typetracker"&gt;Detecting column types using TypeTracker&lt;/a&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/445"&gt;#445&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sqlite_utils.utils.chunks()&lt;/code&gt; is now a documented API for &lt;a href="https://sqlite-utils.datasette.io/en/stable/reference.html#reference-utils-chunks"&gt;splitting an iterator into chunks&lt;/a&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/451"&gt;#451&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I have a policy that any time I need to use an undocumented method from &lt;code&gt;sqlite-utils&lt;/code&gt; in some other project I file an issue to add that to the documented API surface.&lt;/p&gt;
&lt;p&gt;I had used &lt;code&gt;rows_from_file()&lt;/code&gt; and &lt;code&gt;TypeTracker&lt;/code&gt; in &lt;a href="https://datasette.io/plugins/datasette-socrata"&gt;datasette-socrata&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sqlite-utils enable-fts&lt;/code&gt; now has a &lt;code&gt;--replace&lt;/code&gt; option for replacing the existing FTS configuration for a table. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/450"&gt;#450&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;create-index&lt;/code&gt;, &lt;code&gt;add-column&lt;/code&gt; and &lt;code&gt;duplicate&lt;/code&gt; commands all now take a &lt;code&gt;--ignore&lt;/code&gt; option for ignoring errors should the database not be in the right state for them to operate. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/450"&gt;#450&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This was inspired by my TIL &lt;a href="https://til.simonwillison.net/bash/ignore-errors"&gt;Ignoring errors in a section of a Bash script&lt;/a&gt; - a trick I had to figure out because one of my scripts needed to add columns and enable FTS but only if those changes had not been previously applied.&lt;/p&gt;
&lt;p&gt;In looking into that I spotted inconsistencies in the design of the &lt;code&gt;sqlite-utils&lt;/code&gt; commands, so I fixed those as much as I could while still maintaining backwards compatibility with the 3.x releases.&lt;/p&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/s3-ocr"&gt;s3-ocr&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/s3-ocr/releases/tag/0.5"&gt;0.5&lt;/a&gt; - (&lt;a href="https://github.com/simonw/s3-ocr/releases"&gt;5 releases total&lt;/a&gt;) - 2022-07-19
&lt;br /&gt;Tools for running OCR against files stored in S3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-graphql"&gt;datasette-graphql&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-graphql/releases/tag/2.1.1"&gt;2.1.1&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-graphql/releases"&gt;36 releases total&lt;/a&gt;) - 2022-07-18
&lt;br /&gt;Datasette plugin providing an automatic GraphQL API for your SQLite databases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-sentry"&gt;datasette-sentry&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-sentry/releases/tag/0.2a0"&gt;0.2a0&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-sentry/releases"&gt;3 releases total&lt;/a&gt;) - 2022-07-18
&lt;br /&gt;Datasette plugin for configuring Sentry&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette"&gt;datasette&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette/releases/tag/0.62a1"&gt;0.62a1&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette/releases"&gt;112 releases total&lt;/a&gt;) - 2022-07-18
&lt;br /&gt;An open source multi-tool for exploring and publishing data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.28"&gt;3.28&lt;/a&gt; - (&lt;a href="https://github.com/simonw/sqlite-utils/releases"&gt;102 releases total&lt;/a&gt;) - 2022-07-15
&lt;br /&gt;Python CLI utility and library for manipulating SQLite databases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-publish-vercel"&gt;datasette-publish-vercel&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-publish-vercel/releases/tag/0.14"&gt;0.14&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-publish-vercel/releases"&gt;21 releases total&lt;/a&gt;) - 2022-07-13
&lt;br /&gt;Datasette plugin for publishing data using Vercel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-app"&gt;datasette-app&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-app/releases/tag/0.2.2"&gt;0.2.2&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-app/releases"&gt;4 releases total&lt;/a&gt;) - 2022-07-13
&lt;br /&gt;The Datasette macOS application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-app-support"&gt;datasette-app-support&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-app-support/releases/tag/0.11.6"&gt;0.11.6&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-app-support/releases"&gt;19 releases total&lt;/a&gt;) - 2022-07-12
&lt;br /&gt;Part of &lt;a href="https://github.com/simonw/datasette-app"&gt;https://github.com/simonw/datasette-app&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/npm/upgrading-packages"&gt;Upgrading packages with npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/electron/testing-electron-playwright"&gt;Testing Electron apps with Playwright and GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/python/pip-tools"&gt;Freezing requirements with pip-tools&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-cloud"&gt;datasette-cloud&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/annotated-release-notes"&gt;annotated-release-notes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-desktop"&gt;datasette-desktop&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="weeknotes"/><category term="datasette-cloud"/><category term="sqlite-utils"/><category term="annotated-release-notes"/><category term="datasette-desktop"/></entry><entry><title>Weeknotes number 100</title><link href="https://simonwillison.net/2021/Sep/19/weeknotes/#atom-tag" rel="alternate"/><published>2021-09-19T01:34:37+00:00</published><updated>2021-09-19T01:34:37+00:00</updated><id>https://simonwillison.net/2021/Sep/19/weeknotes/#atom-tag</id><summary type="html">
    &lt;p&gt;This entry marks my &lt;a href="https://simonwillison.net/tags/weeknotes/"&gt;100th weeknotes&lt;/a&gt;, which I've managed to post once a week (plus or minus a few days) consistently since &lt;a href="https://simonwillison.net/2019/Sep/13/weeknotestwitter-sqlite-datasette-rure/"&gt;13th September 2019&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I started writing weeknotes to add some accountability to the work I was doing during &lt;a href="https://simonwillison.net/2019/Sep/10/jsk-fellowship/"&gt;my JSK fellowship year&lt;/a&gt; at Stanford. The fellowship ended over a year ago but I've stuck to the habit - I've been finding it really helpful as a structured approach to thinking about my work every week, and it occasionally helps motivate me to get things done enough that I have something I can write about!&lt;/p&gt;
&lt;h4&gt;Datasette Desktop 0.2.0&lt;/h4&gt;
&lt;p&gt;My big achievement this week was &lt;a href="https://datasette.io/desktop"&gt;Datasette Desktop 0.2.0&lt;/a&gt; (and the &lt;a href="https://github.com/simonw/datasette-app/releases/tag/0.2.1"&gt;0.2.1 patch release&lt;/a&gt; that followed). I published &lt;a href="https://simonwillison.net/2021/Sep/13/datasette-desktop-2/"&gt;annotated release notes&lt;/a&gt; for that a few days ago. I'm really pleased with the release - I think Datasette as a desktop application is going to significantly increase the impact of the project.&lt;/p&gt;
&lt;p&gt;I also sent out &lt;a href="https://datasette.substack.com/p/datasette-desktop-a-macos-application"&gt;an issue of the Datasette Newsletter&lt;/a&gt; promoting the new desktop application.&lt;/p&gt;
&lt;h4&gt;Datasette Desktop for Windows&lt;/h4&gt;
&lt;p&gt;I did a quick research spike to investigate the feasibility of publishing &lt;a href="https://github.com/simonw/datasette-app/issues/71"&gt;a Windows version&lt;/a&gt; of Datasette Desktop. To my surprise, I managed to get a working prototype going with just half a small amount of work:&lt;/p&gt;

&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;So that was one heck of a lot easier than I expected... &lt;a href="https://t.co/BDa4gvkgnd"&gt;pic.twitter.com/BDa4gvkgnd&lt;/a&gt;&lt;/p&gt;- Simon Willison (@simonw) &lt;a href="https://twitter.com/simonw/status/1438008493265473536?ref_src=twsrc%5Etfw"&gt;September 15, 2021&lt;/a&gt;&lt;/blockquote&gt; 
&lt;p&gt;Electron claims to solve cross-platform development and it seems to uphold that claim pretty well!&lt;/p&gt;
&lt;p&gt;I'm still quite a bit of work away from having a release: I need to learn how to build and sign Windows installers. But this is a very promising first step.&lt;/p&gt;
&lt;h4&gt;json-flatten&lt;/h4&gt;
&lt;p&gt;I've started thinking about how I can enable Datasette Desktop users to configure plugins without having to hand-edit plugin configuration JSON (the current mechanism).&lt;/p&gt;
&lt;p&gt;This made me take another look at a small library I released a couple of years ago, &lt;a href="https://github.com/simonw/json-flatten"&gt;json-flatten&lt;/a&gt;, which turns a nested JSON object into a set of flat key/value pairs suitable for editing using an HTML form and then unflattens that data later on.&lt;/p&gt;
&lt;div class="highlight highlight-text-python-console"&gt;&lt;pre&gt;&amp;gt;&amp;gt;&amp;gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; json_flatten
&amp;gt;&amp;gt;&amp;gt; json_flatten.flatten({&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;foo&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: {&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;bar&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: [&lt;span class="pl-c1"&gt;1&lt;/span&gt;, &lt;span class="pl-c1"&gt;True&lt;/span&gt;, &lt;span class="pl-c1"&gt;None&lt;/span&gt;]}})
{'foo.bar.[0]$int': '1', 'foo.bar.[1]$bool': 'True', 'foo.bar.[2]$none': 'None'}
&amp;gt;&amp;gt;&amp;gt; json_flatten.unflatten(_)
{'foo': {'bar': [1, True, None]}}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It turns out a few people have been using the library, and had filed issues - I released &lt;a href="https://github.com/simonw/json-flatten/releases/tag/0.2"&gt;version 0.2&lt;/a&gt; with a couple of fixes.&lt;/p&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/sql/cumulative-total-over-time"&gt;Cumulative total over time in SQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/electron/electrion-auto-update"&gt;Configuring auto-update for an Electron app&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-statistics"&gt;datasette-statistics&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-statistics/releases/tag/0.1.1"&gt;0.1.1&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-statistics/releases"&gt;2 releases total&lt;/a&gt;) - 2021-09-16
&lt;br /&gt;SQL statistics functions for Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/json-flatten"&gt;json-flatten&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/json-flatten/releases/tag/0.2"&gt;0.2&lt;/a&gt; - 2021-09-14
&lt;br /&gt;Python functions for flattening a JSON object to a single dictionary of pairs, and unflattening that dictionary back to a JSON object&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-app"&gt;datasette-app&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-app/releases/tag/0.2.1"&gt;0.2.1&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-app/releases"&gt;3 releases total&lt;/a&gt;) - 2021-09-13
&lt;br /&gt;The Datasette macOS application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-app-support"&gt;datasette-app-support&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-app-support/releases/tag/0.11.5"&gt;0.11.5&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-app-support/releases"&gt;18 releases total&lt;/a&gt;) - 2021-09-13
&lt;br /&gt;Part of &lt;a href="https://github.com/simonw/datasette-app"&gt;https://github.com/simonw/datasette-app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-write"&gt;datasette-write&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-write/releases/tag/0.2"&gt;0.2&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-write/releases"&gt;3 releases total&lt;/a&gt;) - 2021-09-11
&lt;br /&gt;Datasette plugin providing a UI for executing SQL writes against the database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-schema-versions"&gt;datasette-schema-versions&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-schema-versions/releases/tag/0.2"&gt;0.2&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-schema-versions/releases"&gt;2 releases total&lt;/a&gt;) - 2021-09-11
&lt;br /&gt;Datasette plugin that shows the schema version of every attached database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-import-table"&gt;datasette-import-table&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-import-table/releases/tag/0.3"&gt;0.3&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-import-table/releases"&gt;6 releases total&lt;/a&gt;) - 2021-09-08
&lt;br /&gt;Datasette plugin for importing tables from other Datasette instances&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-desktop"&gt;datasette-desktop&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="datasette"/><category term="weeknotes"/><category term="datasette-desktop"/></entry><entry><title>Datasette Desktop 0.2.0: The annotated release notes</title><link href="https://simonwillison.net/2021/Sep/13/datasette-desktop-2/#atom-tag" rel="alternate"/><published>2021-09-13T23:30:24+00:00</published><updated>2021-09-13T23:30:24+00:00</updated><id>https://simonwillison.net/2021/Sep/13/datasette-desktop-2/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;a href="https://datasette.io/desktop"&gt;Datasette Desktop&lt;/a&gt; is a new macOS desktop application version of &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt;, an "open source multi-tool for exploring and publishing data" built on top of SQLite. I released the first version &lt;a href="https://simonwillison.net/2021/Sep/8/datasette-desktop/"&gt;last week&lt;/a&gt; - I've just released version 0.2.0 (and a 0.2.1 bug fix) with a whole bunch of critical improvements.&lt;/p&gt;
&lt;p&gt;You can see the &lt;a href="https://github.com/simonw/datasette-app/releases/tag/0.2.0"&gt;release notes for 0.2.0 here&lt;/a&gt;, but as I've done &lt;a href="https://simonwillison.net/series/datasette-release-notes/"&gt;with Datasette in the past&lt;/a&gt; I've decided to present an annotated version of those release notes providing further background on each of the new features.&lt;/p&gt;
&lt;h4&gt;The plugin directory&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;A new &lt;strong&gt;plugin directory&lt;/strong&gt; for installing new plugins and upgrading or uninstalling existing ones. Open it using the "Plugins -&amp;gt; Install and Manage Plugins..." menu item. &lt;a href="https://github.com/simonw/datasette-app/issues/74"&gt;#74&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2021/plugin-directory.gif" alt="Demo showing installing and upgrading a plugin" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This was the main focus for the release. &lt;a href="https://datasette.io/plugins"&gt;Plugins&lt;/a&gt; are a key component of both Datasette and Datasette Desktop: my goal is for Datasette to provide a robust core for exploring databases, with a wide array of plugins that support any additional kind of visualization, exploration or data manipulation capability that a user might want.&lt;/p&gt;
&lt;p&gt;Datasette Desktop goes as far as bundling &lt;a href="https://simonwillison.net/2021/Sep/8/datasette-desktop/#how-the-app-works"&gt;an entire standalone Python installation&lt;/a&gt; just to ensure that plugins will work correctly, and invisibly sets up a dedicated Python virtual environment for plugins to install into when you first run the application.&lt;/p&gt;
&lt;p&gt;The first version of the app allowed users to install plugins by pasting their name into a text input field. Version 0.2.0 is a whole lot more sophisticated: the single input field has been replaced by a full plugin directory interface that shows installed v.s. available plugins and provides "Install", "Upgrade" and "Uninstall" buttons depending on the state of the plugin.&lt;/p&gt;
&lt;p&gt;When I set out to build this I knew I wanted to hit &lt;a href="https://datasette.io/content/plugins.json?_shape=array"&gt;this JSON API&lt;/a&gt; on &lt;a href="https://datasette.io/"&gt;datasette.io&lt;/a&gt; to fetch the list of plugins, and I knew I wanted a simple searchable index page. The I realized I also wanted faceted search, so I could filter for installed vs not-yet-installed plugins.&lt;/p&gt;
&lt;p&gt;Datasette's built-in table interface already implements faceted search! So I decided to use that, with &lt;a href="https://github.com/simonw/datasette-app-support/tree/0.11.5/datasette_app_support/templates"&gt;some custom templates&lt;/a&gt; to add the install buttons and display the plugins in a more suitable format.&lt;/p&gt;
&lt;p&gt;The first challenge was getting the latest list of plugins into my Datasette instance. I built this into the &lt;code&gt;datasette-app-support&lt;/code&gt; plugin using the &lt;a href="https://docs.datasette.io/en/stable/plugin_hooks.html#startup-datasette"&gt;startup() plugin hook&lt;/a&gt; - every time the server starts up it hits that API and &lt;a href="https://github.com/simonw/datasette-app-support/blob/ee8a05ba1dd55734d2b99c9bd774ebb8e9790d7c/datasette_app_support/__init__.py#L17-L62"&gt;populates an in-memory table&lt;/a&gt; with the returned data.&lt;/p&gt;
&lt;p&gt;The data from the API is then extended with four extra columns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"installed"&lt;/code&gt; is set to "installed" or "not installed" depending on whether the plugin has already been installed by the user&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"Installed_version"&lt;/code&gt; is the currently installed version of the plugin&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"upgrade"&lt;/code&gt; is the string "upgrade available" or None - allowing the user to filter for just plugins that can be upgraded&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"default"&lt;/code&gt; is set to 1 if the plugin is a default plugin that came with Datasette&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The data needed to build the plugin table is gathered by these three lines of code:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;plugins&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;httpx&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(
     &lt;span class="pl-s"&gt;"https://datasette.io/content/plugins.json?_shape=array"&lt;/span&gt;
).&lt;span class="pl-en"&gt;json&lt;/span&gt;()
&lt;span class="pl-c"&gt;# Annotate with list of installed plugins&lt;/span&gt;
&lt;span class="pl-s1"&gt;installed_plugins&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; {
    &lt;span class="pl-s1"&gt;plugin&lt;/span&gt;[&lt;span class="pl-s"&gt;"name"&lt;/span&gt;]: &lt;span class="pl-s1"&gt;plugin&lt;/span&gt;[&lt;span class="pl-s"&gt;"version"&lt;/span&gt;]
    &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;plugin&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; (&lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;datasette&lt;/span&gt;.&lt;span class="pl-s1"&gt;client&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"/-/plugins.json"&lt;/span&gt;)).&lt;span class="pl-en"&gt;json&lt;/span&gt;()
}
&lt;span class="pl-s1"&gt;default_plugins&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; (&lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-s1"&gt;environ&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"DATASETTE_DEFAULT_PLUGINS"&lt;/span&gt;) &lt;span class="pl-c1"&gt;or&lt;/span&gt; &lt;span class="pl-s"&gt;""&lt;/span&gt;).&lt;span class="pl-en"&gt;split&lt;/span&gt;()&lt;/pre&gt;
&lt;p&gt;The first line fetches the full list of known plugins from the Datasette &lt;a href="https://datasette.io/plugins"&gt;plugin directory&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The second makes an internal API call to the Datasette &lt;code&gt;/-/plugins.json&lt;/code&gt; endpoint using the &lt;a href="https://docs.datasette.io/en/stable/internals.html#datasette-client"&gt;datasette.client mechanism&lt;/a&gt; to discover what plugins are currently installed and their versions.&lt;/p&gt;
&lt;p&gt;The third line loads a space-separated list of default plugins from the &lt;code&gt;DATASETTE_DEFAULT_PLUGINS&lt;/code&gt; environment variable.&lt;/p&gt;
&lt;p&gt;That last one deserves further explanation. Datasette Desktop now ships with some default plugins, and the point of truth for what those are &lt;a href="https://github.com/simonw/datasette-app/blob/0.2.0/main.js#L34-L42"&gt;lives in the Electron app codebase&lt;/a&gt; - because that's where the code responsible for installing them is.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Five plugins are now installed by default: &lt;a href="https://datasette.io/plugins/datasette-vega"&gt;datasette-vega&lt;/a&gt;, &lt;a href="https://datasette.io/plugins/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt;, &lt;a href="https://datasette.io/plugins/datasette-pretty-json"&gt;datasette-pretty-json&lt;/a&gt;, &lt;a href="https://datasette.io/plugins/datasette-edit-schema"&gt;datasette-edit-schema&lt;/a&gt; and &lt;a href="https://datasette.io/plugins/datasette-configure-fts"&gt;datasette-configure-fts&lt;/a&gt;. &lt;a href="https://github.com/simonw/datasette-app/issues/81"&gt;#81&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The plugin directory needs to know what these defaults are so it can avoid showing the "uninstall" button for those plugins. Uninstalling them currently makes no sense because Datasette Desktop installs any missing dependencies when the app starts, which would instantly undo the user's uninstall action decision.&lt;/p&gt;
&lt;p&gt;An environment variable felt like the most straight-forward way to expose that list of default plugins to the underlying Datasette server!&lt;/p&gt;
&lt;p&gt;I plan to make default plugins uninstallable in the future but doing so require a mechanism for persisting user preference state which I haven't built yet (see &lt;a href="https://github.com/simonw/datasette-app/issues/101"&gt;issue #101&lt;/a&gt;).&lt;/p&gt;
&lt;h4&gt;A log on the loading screen&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;The application &lt;strong&gt;loading screen&lt;/strong&gt; now shows a log of what is going on. &lt;a href="https://github.com/simonw/datasette-app/issues/70"&gt;#70&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The first time you launch the Datasette Desktop application it creates a virtual environment and installs &lt;a href="https://github.com/simonw/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://github.com/simonw/datasette-app-support"&gt;datasette-app-support&lt;/a&gt; and the five default plugins (plus their dependencies) into that environment.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2021/datasette-launch-log.gif" alt="Animated demo of the Datasette Desktop launch screen showing the log scrolling past" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This can take quite a few seconds, during which the original app would show an indeterminate loading indicator.&lt;/p&gt;
&lt;p&gt;Personally I hate loading indicators which don't show the difference between something that's working and something that's eternally hung. Since I can't estimate how long it will take, I decided to pipe the log of what the &lt;code&gt;pip install&lt;/code&gt; command is doing to the loading screen itself.&lt;/p&gt;
&lt;p&gt;For most users this will be meaningless, but hopefully will help communicate "I'm installing extra stuff that I need". Advanced users may find this useful though, especially for bug reporting if something goes wrong.&lt;/p&gt;
&lt;p&gt;Under the hood &lt;a href="https://github.com/simonw/datasette-app/commit/e0c899e422e60b43866551fd776b86a954deb94d"&gt;I implemented this&lt;/a&gt; using a Node.js &lt;a href="https://nodejs.org/api/events.html#events_class_eventemitter"&gt;EventEmitter&lt;/a&gt;. I use the same trick to forward server log output to the "Debug -&amp;gt; Show Sever Log" interface.&lt;/p&gt;
&lt;h4&gt;Example CSV files&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;The welcome screen now invites you to try out the application by opening interesting &lt;strong&gt;example CSV files&lt;/strong&gt;, taking advantage of the new "File -&amp;gt; Open CSV from URL..." feature. &lt;a href="https://github.com/simonw/datasette-app/issues/91"&gt;#91&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Previously Datasette Desktop wouldn't do anything at all until you opened up a CSV or SQLite database, and I have a hunch that unlike me most people don't have good examples of those to hand at all times!&lt;/p&gt;
&lt;p&gt;The new welcome screen offers example CSV files that can be opened directly from the internet. I implemented this using a new API at &lt;a href="https://datasette.io/content/example_csvs"&gt;datasette.io/content/example_csvs&lt;/a&gt; (add &lt;code&gt;.json&lt;/code&gt; for the JSON version) which is loaded by code running on that welcome page.&lt;/p&gt;
&lt;p&gt;I have two examples at the moment, for &lt;a href="https://www.thesquirrelcensus.com/"&gt;the Squirrel Census&lt;/a&gt; and the &lt;a href="https://data.london.gov.uk/dataset/animal-rescue-incidents-attended-by-lfb"&gt;London Fire Brigade's animal rescue data&lt;/a&gt;. I'll be adding more in the future.&lt;/p&gt;
&lt;p&gt;The API itself is a great example of the &lt;a href="https://simonwillison.net/2021/Jul/28/baked-data/"&gt;Baked Data architectural pattern&lt;/a&gt; in action: the data itself is stored in &lt;a href="https://github.com/simonw/datasette.io/blob/main/example_csvs.yml"&gt;this hand-edited YAML file&lt;/a&gt;, which is compiled to SQLite every time the site is deployed.&lt;/p&gt;
&lt;p&gt;To get this feature working I added a new "Open CSV from URL" capability to the app, which is also available in the File menu. Under the hood this works by passing the provided URL to the new &lt;code&gt;/-/open-csv-from-url&lt;/code&gt; API endpoint. The implementation of this &lt;a href="https://github.com/simonw/datasette-app-support/blob/0.11.5/datasette_app_support/utils.py#L52-L88"&gt;was surprisingly fiddly&lt;/a&gt; as I wanted to consume the CSV file using an asynchronous HTTP client - I ended up using an adaption of &lt;a href="https://github.com/mosquito/aiofile/blob/3.5.1/README.rst#async-csv-dict-reader"&gt;some example code&lt;/a&gt; from the &lt;a href="https://github.com/mosquito/aiofile"&gt;aiofile&lt;/a&gt; README.&lt;/p&gt;
&lt;h4&gt;Recently opened files and "Open with Datasette"&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Recently opened&lt;/strong&gt; &lt;code&gt;.db&lt;/code&gt; and &lt;code&gt;.csv&lt;/code&gt; files can now be accessed from the new "File -&amp;gt; Open Recent" menu. Thanks, &lt;a href="https://github.com/mnckapilan"&gt;Kapilan M&lt;/a&gt;! &lt;a href="https://github.com/simonw/datasette-app/issues/54"&gt;#54&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This was the project's first external contribution! Kapilan M &lt;a href="https://github.com/simonw/datasette-app/pull/77"&gt;figured out a way&lt;/a&gt; to hook into the macOS "recent files" mechanism from Electron, and I expanded that to cover SQLite database in addition to CSV files.&lt;/p&gt;
&lt;p&gt;When a recent file is selected, Electron fires &lt;a href="https://www.electronjs.org/docs/api/app#event-open-file-macos"&gt;the "open-file" event&lt;/a&gt;. This same event is fired when a file is opened using "Open With -&amp;gt; Datasette" or dragged onto the application's dock.&lt;/p&gt;
&lt;p&gt;This meant I needed to tell the difference between a CSV or a SQLite database file, which I do by &lt;a href="https://github.com/simonw/datasette-app/blob/0.2.0/main.js#L95-L101"&gt;checking if&lt;/a&gt; the first 16 bytes of the file match the SQLite header of &lt;code&gt;SQLite format 3\0&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;.db and .csv files&lt;/strong&gt; can now be opened in Datasette starting from the Finder using "Right Click -&amp;gt; Open With -&amp;gt; Datasette". &lt;a href="https://github.com/simonw/datasette-app/issues/40"&gt;#40&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Registering Datasette as a file handler for &lt;code&gt;.csv&lt;/code&gt; and &lt;code&gt;.db&lt;/code&gt; was not at all obvious. It turned out to involve adding the following to the Electron app's &lt;a href="https://github.com/simonw/datasette-app/blob/0.2.0/package.json#L13-L28"&gt;package.json file&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;build&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: {
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;appId&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;io.datasette.app&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;mac&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: {
      &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;category&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;public.app-category.developer-tools&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;extendInfo&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: {
        &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;CFBundleDocumentTypes&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: [
          {
            &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;CFBundleTypeExtensions&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: [
              &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;csv&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
              &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;tsv&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
              &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;db&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
            ],
            &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;LSHandlerRank&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Alternate&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
          }
        ]
      }&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;The Debug Menu&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;A new &lt;strong&gt;Debug menu&lt;/strong&gt; can be enabled using Datasette -&amp;gt; About Datasette -&amp;gt; Enable Debug Menu".&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The debug menu existed previously in development mode, but with 0.2.0 I decided to expose it to end users. I didn't want to show it to people who weren't ready to see it, so you have to first enable it using a button on the about menu.&lt;/p&gt;
&lt;p&gt;The most interesting option there is "Run Server Manually".&lt;/p&gt;
&lt;p&gt;Most of the time when you are using the app there's a &lt;code&gt;datasette&lt;/code&gt; Python server running under the hood, but it's entirely managed by the Node.js &lt;a href="https://nodejs.org/api/child_process.html"&gt;child_process&lt;/a&gt; module.&lt;/p&gt;
&lt;p&gt;When developing the application (or associated plugins) it can be useful to manually run that server rather than having it managed by the app, so you can see more detailed error messages or even add the &lt;code&gt;--pdb&lt;/code&gt; option to drop into a debugger should something go wrong.&lt;/p&gt;
&lt;p&gt;To run that server, you need the Electron app to kill its own version... and you then need to know things like what port it was running on and which environment variables it was using.&lt;/p&gt;
&lt;p&gt;Here's what you see when you click the "Run Server Manually" debug option:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2021/run-server-manually.png" alt="Run server manually? Clicking OK will terminate the Datasette server used by this app. Copy this command to a terminal to manually run a replacement" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Here's that command in full:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DATASETTE_API_TOKEN="0ebb45444ba4cbcbacdbcbb989bb0cd3aa10773c0dfce73c0115868d0cee2afa" DATASETTE_SECRET="4a8ac89d0d269c31d99059933040b4511869c12dfa699a1429ea29ee3310a850" DATASETTE_DEFAULT_PLUGINS="datasette datasette-app-support datasette-vega datasette-cluster-map datasette-pretty-json datasette-edit-schema datasette-configure-fts datasette-leaflet" /Users/simon/.datasette-app/venv/bin/datasette --port 8002 --version-note xyz-for-datasette-app --setting sql_time_limit_ms 10000 --setting max_returned_rows 2000 --setting facet_time_limit_ms 3000 --setting max_csv_mb 0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This is a simulation of the command that the app itself used to launch the server. Pasting that into a terminal will produce an exact copy of the original process - and you can add &lt;code&gt;--pdb&lt;/code&gt; or other options to further customize it.&lt;/p&gt;
&lt;h4&gt;Bonus: Restoring the in-memory database on restart&lt;/h4&gt;
&lt;p&gt;This didn't make it into the formal release notes, but it's a fun bug that I fixed in this release.&lt;/p&gt;
&lt;p&gt;Datasette Desktop defaults to opening CSV files in an in-memory database. You can import them into an on-disk database too, but if you just want to start exploring CSV data in Datasette I decided an in-memory database would be a better starting point.&lt;/p&gt;
&lt;p&gt;There's one problem with this: installing a plugin requires a Datasette server restart, and restarting the server clears the content of that in-memory database, causing any tables created from imported CSVs to disappear. This is confusing!&lt;/p&gt;
&lt;p&gt;You can follow my progress on this in issue &lt;a href="https://github.com/simonw/datasette-app/issues/42"&gt;#42: If you open a CSV and then install a plugin the CSV table vanishes&lt;/a&gt;. I ended up solving it by adding code that dumps the "temporary" in-memory database to a file on disk before a server restart, restarts the server, then copies that disk backup into memory again.&lt;/p&gt;
&lt;p&gt;This works using two custom API endpoints added to the &lt;a href="https://github.com/simonw/datasette-app-support"&gt;datasette-app-support&lt;/a&gt; plugin:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POST /-/dump-temporary-to-file&lt;/code&gt; with &lt;code&gt;{"path": "/path/to/backup.db"}&lt;/code&gt; dumps the contents of that in-memory temporary database to the specified file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /-/restore-temporary-from-file&lt;/code&gt; with &lt;code&gt;{"path": "/path/to/backup.db"}&lt;/code&gt; restors the content back again.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These APIs are called from the &lt;a href="https://github.com/simonw/datasette-app/blob/0.2.0/main.js#L189-L221"&gt;startOrRestart()&lt;/a&gt; method any time the server restarts, using a file path generated by Electron using the following:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;backupPath&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;path&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;join&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-s1"&gt;app&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getPath&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"temp"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-s"&gt;`backup-&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;crypto&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;randomBytes&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-c1"&gt;8&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;toString&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"hex"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;.db`&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The file is deleted once it has been restored.&lt;/p&gt;
&lt;p&gt;After much experimentation, I &lt;a href="https://github.com/simonw/datasette-app-support/blob/0.11.5/datasette_app_support/__init__.py#L270-L288"&gt;ended up using&lt;/a&gt; the &lt;code&gt;db.backup(other_connection)&lt;/code&gt; method that was added to Python's &lt;code&gt;sqlite3&lt;/code&gt; module in Python 3.7. Since Datasette Desktop bundles its own copy of Python 3.9 I don't have to worry about compatibility with older versions at all.&lt;/p&gt;
&lt;h4&gt;The rest is in the milestone&lt;/h4&gt;
&lt;p&gt;If you want even more detailed notes on what into the release, each new feature is included in the &lt;a href="https://github.com/simonw/datasette-app/milestone/2?closed=1"&gt;0.2.0 milestone&lt;/a&gt;, accompanied by a detailed issue with screenshots (and even a few videos) plus links to the underlying commits.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/releasenotes"&gt;releasenotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/annotated-release-notes"&gt;annotated-release-notes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/electron"&gt;electron&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-desktop"&gt;datasette-desktop&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="releasenotes"/><category term="datasette"/><category term="annotated-release-notes"/><category term="electron"/><category term="datasette-desktop"/></entry><entry><title>Datasette Desktop - a macOS desktop application for Datasette</title><link href="https://simonwillison.net/2021/Sep/8/datasette-desktop/#atom-tag" rel="alternate"/><published>2021-09-08T19:15:46+00:00</published><updated>2021-09-08T19:15:46+00:00</updated><id>https://simonwillison.net/2021/Sep/8/datasette-desktop/#atom-tag</id><summary type="html">
    &lt;p&gt;I just released &lt;a href="https://datasette.io/desktop"&gt;version 0.1.0&lt;/a&gt; of the new Datasette macOS desktop application, the first version that end-users can easily install. I would very much appreciate your help testing it out!&lt;/p&gt;
&lt;h4&gt;Datasette Desktop&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://datasette.io/static/datasette-desktop.jpg" alt="Datasette Desktop screenshot" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; is "an open source multi-tool for exploring and publishing data". It's a Python web application that lets you explore data held in SQLite databases, plus a growing &lt;a href="https://datasette.io/plugins"&gt;ecosystem of plugins&lt;/a&gt; for visualizing and manipulating those databases.&lt;/p&gt;
&lt;p&gt;Datasette is aimed at data journalists, museum curators, archivists, local governments, scientists, researchers and anyone else who has data that they wish to explore and share with the world.&lt;/p&gt;
&lt;p&gt;There's just one big catch: since it's a Python web application, those users have needed to figure out how to &lt;a href="https://docs.datasette.io/en/stable/installation.html"&gt;install and run&lt;/a&gt; Python software in order to use it. For people who don't live and breath Python and the command-line this turns out to be a substantial barrier to entry!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://datasette.io/desktop"&gt;Datasette Desktop&lt;/a&gt;&lt;/strong&gt; is my latest attempt at addressing this problem. I've packaged up Datasette, SQLite and a full copy of Python such that users can &lt;a href="https://datasette.io/desktop"&gt;download and uncompress a zip file&lt;/a&gt;, drag it into their &lt;code&gt;/Applications&lt;/code&gt; folder and start using Datasette, without needing to know that there's a Python web server running under the hood (or even understand what a Python web server is).&lt;/p&gt;
&lt;p&gt;Please try it out, and send me feedback and suggestions &lt;a href="https://github.com/simonw/datasette-app/discussions/67"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;What the app does&lt;/h4&gt;
&lt;p&gt;This initial release has a small but useful set of features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open an existing SQLite database file and offer all of Datasette's functionality, including the ability to explore tables and to execute arbitrary SQL queries.&lt;/li&gt;
&lt;li&gt;Open a CSV file and offer the Datasette table interface (&lt;a href="https://covid-19.datasettes.com/covid/economist_excess_deaths"&gt;example here&lt;/a&gt;). By default this uses an in-memory database that gets cleared when the app shuts down, or you can...&lt;/li&gt;
&lt;li&gt;Import CSV files into tables in on-disk SQLite databases (including creating a new blank database first).&lt;/li&gt;
&lt;li&gt;By default the application runs a local web server which only accepts connections from your machine... but you can change that in the "File -&amp;gt; Access Control" menu to allow connections from anyone on your network. This includes &lt;a href="https://tailscale.com/"&gt;Tailscale&lt;/a&gt; networks too, allowing you to run the application on your home computer and then access it securely from other devices such as your mobile phone anywhere in the world.&lt;/li&gt;
&lt;li&gt;You can install plugins! This is the most exciting aspect of this initial release: it's already in a state where users can customize it and developers can extend it, either with &lt;a href="https://datasette.io/plugins"&gt;Datasette's existing plugins&lt;/a&gt; (69 and counting) or by &lt;a href="https://docs.datasette.io/en/stable/writing_plugins.html"&gt;writing new ones&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="how-the-app-works"&gt;How the app works&lt;/h4&gt;
&lt;p&gt;There are three components to the app:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A macOS wrapper application&lt;/li&gt;
&lt;li&gt;Datasette itself&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;datasette-app-support&lt;/code&gt; plugin&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first is the macOS application itself. This is currently written with &lt;a href="https://www.electronjs.org/"&gt;Electron&lt;/a&gt;, and bundles a full copy of Python 3.9 (based on &lt;a href="https://github.com/indygreg/python-build-standalone"&gt;python-build-standalone&lt;/a&gt; by Gregory Szorc). Bundling Python is essential: the principal goal of the app is to allow people to use Datasette who aren't ready to figure out how to install their own Python environment. Having an isolated and self-contained Python is also a great way of avoiding making &lt;a href="https://xkcd.com/1987/"&gt;XKCD 1987&lt;/a&gt; even worse.&lt;/p&gt;
&lt;p&gt;The macOS application doesn't actually include Datasette itself. Instead, on first launch it creates a new Python virtual environment (currently in &lt;code&gt;~/.datasette-app/venv&lt;/code&gt;, feedback on that location welcome) and installs the other two components: &lt;a href="https://github.com/simonw/datasette"&gt;Datasette&lt;/a&gt; and the &lt;a href="https://github.com/simonw/datasette-app-support"&gt;datasette-app-support&lt;/a&gt; plugin.&lt;/p&gt;
&lt;p&gt;Having a dedicated virtual environment is what enables the "Install Plugin" menu option. When a plugin is installed the macOS application runs &lt;code&gt;pip install name-of-plugin&lt;/code&gt; and then restarts the Datasette server process, causing it to load that new plugin.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/simonw/datasette-app-support"&gt;datasette-app-support&lt;/a&gt; plugin is designed exclusively to work with this application. It adds API endpoints that the Electron shell can use to trigger specific actions, such as "import from this CSV file" or "attach this SQLite database" - these are generally triggered by macOS application menu items.&lt;/p&gt;
&lt;p&gt;It also adds a custom authentication mechanism. The user of the app should have special permissions: only they should be able to import a CSV file from anywhere on their computer into Datasette. But for the "network share" feature I want other users to be able to access the web application.&lt;/p&gt;
&lt;p&gt;An interesting consequence of installing Datasette on first-run rather than bundling it with the application is that the user will be able to upgrade to future Datasette releases without needing to re-install the application itself.&lt;/p&gt;
&lt;h4&gt;How I built it&lt;/h4&gt;
&lt;p&gt;I've been building this application completely in public over &lt;a href="https://simonwillison.net/2021/Aug/30/datasette-app/"&gt;the past two weeks&lt;/a&gt;, writing up my notes and research in GitHub issues as I went (here's &lt;a href="https://github.com/simonw/datasette-app/milestone/1?closed=1"&gt;the initial release milestone&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I had to figure out a lot of stuff!&lt;/p&gt;
&lt;p&gt;First, &lt;a href="https://www.electronjs.org/"&gt;Electron&lt;/a&gt;. Since almost all of the user-facing interface is provided by the existing Datasette web application, Electron was a natural fit: I needed help powering native menus and bundling everything up as an installable application, which Electron handles extremely well.&lt;/p&gt;
&lt;p&gt;I also have ambitions to &lt;a href="https://github.com/simonw/datasette-app/issues/71"&gt;get a Windows version working&lt;/a&gt; in the future, which should share almost all of the same code.&lt;/p&gt;
&lt;p&gt;Electron also has fantastic &lt;a href="https://www.electronjs.org/docs/tutorial/quick-start"&gt;initial developer onboarding&lt;/a&gt;. I'd love to achieve a similar level of quality for Datasette some day.&lt;/p&gt;
&lt;p&gt;The single biggest challenge was figuring out how to bundle a working copy of the Datasette Python application to run inside the Electron application.&lt;/p&gt;
&lt;p&gt;My initial plan (touched on &lt;a href="https://simonwillison.net/2021/Aug/30/datasette-app/"&gt;last week&lt;/a&gt;) was to compile Datasette and its dependencies into a single executable using &lt;a href="https://www.pyinstaller.org/"&gt;PyInstaller&lt;/a&gt; or &lt;a href="https://pyoxidizer.readthedocs.io/"&gt;PyOxidizer&lt;/a&gt; or &lt;a href="https://py2app.readthedocs.io/"&gt;py2app&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;These tools strip down a Python application to the minimal required set of dependencies and then use various tricks to compress that all into a single binary. They are &lt;em&gt;really clever&lt;/em&gt;. For many projects I imagine this would be the right way to go.&lt;/p&gt;
&lt;p&gt;I had one big problem though: I wanted to &lt;a href="https://github.com/simonw/datasette-app/issues/5"&gt;support plugin installation&lt;/a&gt;. Datasette plugins can have their own dependencies, and could potentially use any of the code from the Python standard library. This means that a stripped-down Python isn't actually right for this project: I need a full installation, standard library and all.&lt;/p&gt;
&lt;p&gt;Telling the user they had to install Python themselves was an absolute non-starter: the entire point of this project is to make Datasette available to users who are unwilling or unable to jump through those hoops.&lt;/p&gt;
&lt;p&gt;Gregory Szorc built PyOxidizer, and as part of that he built &lt;a href="https://python-build-standalone.readthedocs.io/"&gt;python-build-standalone&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This project produces self-contained, highly-portable Python distributions. These Python distributions contain a fully-usable, full-featured Python installation as well as their build artifacts (object files, libraries, etc).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sounds like exactly what I needed! I &lt;a href="https://github.com/simonw/datasette-app/issues/22"&gt;opened a research issue&lt;/a&gt;, built a proof-of-concept and decided to commit to that as the approach I was going to use. Here's a TIL that describes how I'm doing this: &lt;a href="https://til.simonwillison.net/electron/python-inside-electron"&gt;Bundling Python inside an Electron app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;(I find GitHub issue threads to be the ideal way of exploring these kinds of areas. Many of my repositories have a &lt;a href="https://github.com/simonw/datasette-app/labels/research"&gt;research label&lt;/a&gt; specifically to track them.)&lt;/p&gt;
&lt;p&gt;The last key step was figuring out how to sign the application, so I could distribute it to other macOS users without them facing this dreaded dialog:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Datasette.app can't be opened because Apple cannot check it for malicious software" src="https://static.simonwillison.net/static/2021/malicious-software.png" style="max-width:100%; width: 438px" /&gt;&lt;/p&gt;
&lt;p&gt;It turns out there are two steps to this these days: signing the code with a developer certificate, and then "notarizing" it, which involves uploading the bundle to Apple's servers, having them scan it for malicious code and attaching the resulting approval to the bundle.&lt;/p&gt;
&lt;p&gt;I was expecting figuring this out to be a nightmare. It ended up not too bad: I spent two days on it, but most of the work ended up being done by &lt;a href="https://www.electron.build/"&gt;electron-builder&lt;/a&gt; - one of the biggest advantages of working within the Electron ecosystem is that a lot of people have put a lot of effort into these final steps.&lt;/p&gt;
&lt;p&gt;I was adamant that my eventual signing and notarization solution should be automated using GitHub Actions: nothing defangs a frustrating build process more than good
automation! This made things a bit harder because all of the tutorials and documentation assumed you were working with a GUI, but I &lt;a href="https://github.com/simonw/datasette-app/blob/main/.github/workflows/release.yml"&gt;got there in the end&lt;/a&gt;. I wrote this all up as a TIL: &lt;a href="https://til.simonwillison.net/electron/sign-notarize-electron-macos"&gt;Signing and notarizing an Electron app for distribution using GitHub Actions&lt;/a&gt; (see also &lt;a href="https://til.simonwillison.net/github-actions/attach-generated-file-to-release"&gt;Attaching a generated file to a GitHub release using Actions&lt;/a&gt;).&lt;/p&gt;
&lt;h4&gt;What's next&lt;/h4&gt;
&lt;p&gt;I announced the release &lt;a href="https://twitter.com/simonw/status/1435457848050257925"&gt;last night on Twitter&lt;/a&gt; and I've already started getting feedback. This has resulted in a growing number of issues under the &lt;a href="https://github.com/simonw/datasette-app/issues?q=is%3Aissue+is%3Aopen+label%3Ausability"&gt;usability label&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My expectation is that most improvements made for the benefit of Datasette Desktop will benefit the regular Datasette web application too.&lt;/p&gt;
&lt;p&gt;There's also a strategic component to this. I'm investing a lot of development work in Datasette, and I want that work to have the biggest impact possible. Datasette Desktop is an important new distribution channel, which also means that any time I add a new feature to Datasette or build a new plugin the desktop application should see the same benefit as the hosted web application.&lt;/p&gt;
&lt;p&gt;If I'm unlucky I'll find this slows me down: every feature I build will need to include consideration as to how it affects the desktop application.&lt;/p&gt;
&lt;p&gt;My intuition currently is that this trade-off will be worthwhile: I don't think ensuring desktop compatibility will be a significant burden, and the added value from getting new features almost for free through a whole separate distribution channel should hopefully be huge!&lt;/p&gt;

&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/purpleair/purple-air-aqi"&gt;Calculating the AQI based on the Purple Air API for a sensor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/electron/electron-debugger-console"&gt;Using the Chrome DevTools console as a REPL for an Electron app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/electron/electron-external-links-system-browser"&gt;Open external links in an Electron app using the system browser&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/github-actions/attach-generated-file-to-release"&gt;Attaching a generated file to a GitHub release using Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/electron/sign-notarize-electron-macos"&gt;Signing and notarizing an Electron app for distribution using GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/electron/python-inside-electron"&gt;Bundling Python inside an Electron app&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-import-table"&gt;datasette-import-table&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-import-table/releases/tag/0.3"&gt;0.3&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-import-table/releases"&gt;6 releases total&lt;/a&gt;) - 2021-09-08
&lt;br /&gt;Datasette plugin for importing tables from other Datasette instances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-app"&gt;datasette-app&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-app/releases/tag/0.1.0"&gt;Datasette Desktop 0.1.0&lt;/a&gt; - 2021-09-08
&lt;br /&gt;Electron app wrapping Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-app-support"&gt;datasette-app-support&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-app-support/releases/tag/0.6"&gt;0.6&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-app-support/releases"&gt;8 releases total&lt;/a&gt;) - 2021-09-07
&lt;br /&gt;Part of &lt;a href="https://github.com/simonw/datasette-app"&gt;https://github.com/simonw/datasette-app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/pids"&gt;pids&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/pids/releases/tag/0.1.2"&gt;0.1.2&lt;/a&gt; - 2021-09-07
&lt;br /&gt;A tiny Python library for generating public IDs from integers&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/electron"&gt;electron&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-desktop"&gt;datasette-desktop&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="datasette"/><category term="weeknotes"/><category term="github-actions"/><category term="electron"/><category term="datasette-desktop"/></entry><entry><title>Datasette Desktop 0.1.0</title><link href="https://simonwillison.net/2021/Sep/8/datasette-desktop-release/#atom-tag" rel="alternate"/><published>2021-09-08T05:14:32+00:00</published><updated>2021-09-08T05:14:32+00:00</updated><id>https://simonwillison.net/2021/Sep/8/datasette-desktop-release/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-app/releases/tag/0.1.0"&gt;Datasette Desktop 0.1.0&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is the first installable version of the new Datasette Desktop macOS application I’ve been building. Please try it out and leave feedback on Twitter or on the GitHub Discussions thread linked from the release notes.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/simonw/status/1435457848050257925"&gt;@simonw&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/electron"&gt;electron&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-desktop"&gt;datasette-desktop&lt;/a&gt;&lt;/p&gt;



</summary><category term="projects"/><category term="datasette"/><category term="electron"/><category term="datasette-desktop"/></entry><entry><title>Building a desktop application for Datasette (and weeknotes)</title><link href="https://simonwillison.net/2021/Aug/30/datasette-app/#atom-tag" rel="alternate"/><published>2021-08-30T05:13:54+00:00</published><updated>2021-08-30T05:13:54+00:00</updated><id>https://simonwillison.net/2021/Aug/30/datasette-app/#atom-tag</id><summary type="html">
    &lt;p&gt;This week I started experimenting with a desktop application version of &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; - with the goal of providing people who aren't comfortable with the command-line the ability to get Datasette up and running on their own personal computers.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Update 8th September 2021&lt;/strong&gt;: I made a bunch more progress over the week following this post, see &lt;a href="https://simonwillison.net/2021/Sep/8/datasette-desktop/"&gt;Datasette Desktop—a macOS desktop application for Datasette&lt;/a&gt; for details or &lt;a href="https://datasette.io/desktop"&gt;download the app&lt;/a&gt; to try it out.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2021/datasette-app.png" style="max-width: 100%" alt="Screenshot of the new Datasette desktop app prototype with several open windows" /&gt;&lt;/p&gt;
&lt;h4&gt;Why a desktop application?&lt;/h4&gt;
&lt;p&gt;On Monday I &lt;a href="https://twitter.com/simonw/status/1429931719587598346"&gt;kicked off an enormous Twitter conversation&lt;/a&gt; when I posted:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I wonder how much of the popularity of R among some communities in comparison to Python comes down to the fact that with R you can install the RStudio desktop application and you're ready to go&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This ties into my single biggest complaint about Python: it's just too hard for people to get started with. Setting up a Python development environment for the first time remains an enormous barrier to entry.&lt;/p&gt;
&lt;p&gt;I later put this &lt;a href="https://twitter.com/simonw/status/1429937655450505248"&gt;in stronger terms&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The more I think about this the more frustrated I get, thinking about the enormous amount of human potential that's squandered because the barriers to getting started learning to program are so much higher than they need to be&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Which made me think of &lt;a href="https://en.wikipedia.org/wiki/Those_who_live_in_glass_houses_should_not_throw_stones"&gt;glass houses&lt;/a&gt;. My own &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; project has exactly the same problem: to run it locally you need to install Python &lt;em&gt;and then&lt;/em&gt; install Datasette! Mac users &lt;a href="https://docs.datasette.io/en/stable/installation.html"&gt;can use Homebrew&lt;/a&gt;, but telling newcomers to install Homebrew first isn't particularly welcoming either.&lt;/p&gt;
&lt;p&gt;Ideally, I'd like people to be able to install a regular desktop application and start using Datasette that way, without even needing to know that it's written in Python.&lt;/p&gt;
&lt;p&gt;There's been &lt;a href="https://github.com/simonw/datasette/issues/93"&gt;an open issue&lt;/a&gt; to get Datasette running as a standalone binary using PyInstaller since November 2017, with quite a bit of research.&lt;/p&gt;
&lt;p&gt;But I want a UI as well: I don't want to have to teach new users how to install and run a command-line application if I can avoid it.&lt;/p&gt;
&lt;p&gt;So I decided to spend some time researching &lt;a href="https://www.electronjs.org/"&gt;Electron&lt;/a&gt; to see how hard it would be to make a basic Datasette desktop application a reality.&lt;/p&gt;
&lt;h4&gt;Progress so far&lt;/h4&gt;
&lt;p&gt;The code I've written so far can be found in the &lt;a href="https://github.com/simonw/datasette.app"&gt;simonw/datasette.app&lt;/a&gt; repository on GitHub. The app so far does the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run a &lt;code&gt;datasette&lt;/code&gt; server on localhost attached to an available port (found using &lt;a href="https://www.npmjs.com/package/portfinder"&gt;portfinder&lt;/a&gt;) which terminates when the app quits.&lt;/li&gt;
&lt;li&gt;Open a desktop window showing that Datasette instance once the server has started.&lt;/li&gt;
&lt;li&gt;Allow additional windows onto the same instance to be opened using the "New Window" menu option or the Command+N keyboard shortcut.&lt;/li&gt;
&lt;li&gt;Provides an "Open Database..." menu option (and Command+O shortcut) which brings up a file picker to allow the user to select a SQLite database file to open - once selected, this is attached to the Datasette instance and any windows showing the Datasette homepage are reloaded.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's &lt;a href="https://www.youtube.com/watch?v=n90Cg_9j9XE"&gt;a video demo&lt;/a&gt; showing these features in action:&lt;/p&gt;
&lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="allowfullscreen" frameborder="0" height="315" src="https://www.youtube-nocookie.com/embed/n90Cg_9j9XE" style="max-width: 100%" title="YouTube video player" width="560"&gt; &lt;/iframe&gt;
&lt;p&gt;It's very much an MVP, but I'm encouraged by the progress so far. I think this is enough of a proof of concept to be worth turning this into an actual usable product.&lt;/p&gt;
&lt;h4&gt;How this all works&lt;/h4&gt;
&lt;p&gt;There are two components to the application.&lt;/p&gt;
&lt;p&gt;The first is a thin Electron shell, responsible for launching the Python server, managing windows and configuring the various desktop menu options used to configure it. The code for that lives in &lt;a href="https://github.com/simonw/datasette.app/blob/4c968ec5b2b845c644c88425f2d43821cc63c4ff/main.js"&gt;main.js&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The second is a custom Datasette plugin that adds extra functionality needed by the application. Currently this consists of a tiny bit of &lt;a href="https://github.com/simonw/datasette.app/blob/4c968ec5b2b845c644c88425f2d43821cc63c4ff/datasette-app-support/datasette_app_support/static/sticky-footer.css"&gt;extra CSS&lt;/a&gt; to make the footer stick to the bottom of the window, and a &lt;a href="https://github.com/simonw/datasette.app/blob/4c968ec5b2b845c644c88425f2d43821cc63c4ff/datasette-app-support/datasette_app_support/__init__.py#L15-L56"&gt;custom API endpoint&lt;/a&gt; at &lt;code&gt;/-/open-database-file&lt;/code&gt; which is called by the menu option for opening a new database.&lt;/p&gt;
&lt;h4&gt;Initial impressions of Electron&lt;/h4&gt;
&lt;p&gt;I know it's cool to knock Electron, but in this case it feels like exactly the right tool for the job. Datasette is already a web application - what I need is a way to hide the configuration of that web application behind an icon, and re-present the interface in a way that feels more like a desktop application.&lt;/p&gt;
&lt;p&gt;This is my first time building anything with Electron - here are some of my initial impressions.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The initial getting started workflow is really good. I started out with their &lt;a href="https://www.electronjs.org/docs/tutorial/quick-start"&gt;Quick Start&lt;/a&gt; and was up and running with a barebones application that I could start making changes to in just a few minutes.&lt;/li&gt;
&lt;li&gt;The documentation is pretty good, but it leans more towards being an API reference. I found myself googling for examples of different things I wanted to do pretty often.&lt;/li&gt;
&lt;li&gt;The automated testing situation isn't great. I'm using &lt;a href="https://www.electronjs.org/spectron"&gt;Spectron&lt;/a&gt; and &lt;a href="https://mochajs.org/"&gt;Mocha&lt;/a&gt; for &lt;a href="https://github.com/simonw/datasette.app/blob/4c968ec5b2b845c644c88425f2d43821cc63c4ff/test/spec.js"&gt;my initial&lt;/a&gt; (very thin) tests - I got them up and running in GitHub Actions, but I've already run into some limitations:
&lt;ul&gt;
&lt;li&gt;For some reason each time I run the tests an Electron window (and &lt;code&gt;datasette&lt;/code&gt; Python process) is left running. I can't figure out why this is.&lt;/li&gt;
&lt;li&gt;There doesn't appear to be a way for tests to trigger menu items, which is frustrating because most of the logic I've written so far deals with menu items! There is an &lt;a href="https://github.com/electron-userland/spectron/issues/21"&gt;open issue for this&lt;/a&gt; dating back to May 2016.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;I haven't yet managed to package my app. This is clearly going to be the biggest challenge.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Up next: packaging the app&lt;/h4&gt;
&lt;p&gt;I was hoping to get to this before writing up my progress in these weeknotes, but it looks like it's going to be quite a challenge.&lt;/p&gt;
&lt;p&gt;In order to produce an installable macOS app (I'll dive into Windows later) I need to do the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Build a standalone Datasette executable, complete with the custom plugin, using PyInstaller&lt;/li&gt;
&lt;li&gt;Sign that binary with an Apple developer certificate&lt;/li&gt;
&lt;li&gt;Build an Electron application that bundles a copy of that &lt;code&gt;datasette&lt;/code&gt; binary&lt;/li&gt;
&lt;li&gt;Sign the resulting Electron application&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'm expecting figuring this out to be a long-winded and frustrating experience, which is more the fault of Apple than of Electron. I'm tracking my progress on this in &lt;a href="https://github.com/simonw/datasette.app/issues/7"&gt;issue #7&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Datasette 0.59a2&lt;/h4&gt;
&lt;p&gt;I pushed out &lt;a href="https://github.com/simonw/datasette/releases/tag/0.59a2"&gt;a new alpha&lt;/a&gt; of Datasette earlier this week, partly driven by work I was doing on &lt;code&gt;datasette.app&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The biggest new feature in this release is a new plugin hook: &lt;a href="https://docs.datasette.io/en/latest/plugin_hooks.html#plugin-hook-register-commands"&gt;register_commands()&lt;/a&gt; - which lets plugins add additional commands to Datasette, e.g. &lt;code&gt;datasette verify name-of-file.db&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I released a new plugin that exercises this hook called &lt;a href="https://datasette.io/plugins/datasette-verify"&gt;datasette-verify&lt;/a&gt;. Past experience has shown me that it's crucial to ship an example plugin alongside a new hook, to help confirm that the hook design is fit for purpose.&lt;/p&gt;
&lt;p&gt;It turns out I didn't need this for &lt;code&gt;datasette.app&lt;/code&gt; after all, but it's still a great capability to have!&lt;/p&gt;
&lt;h4&gt;sqlite-utils 3.17&lt;/h4&gt;
&lt;p&gt;Quoting the &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-17"&gt;release notes&lt;/a&gt; in full:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-memory"&gt;sqlite-utils memory&lt;/a&gt; command has a new &lt;code&gt;--analyze&lt;/code&gt; option, which runs the equivalent of the &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-analyze-tables"&gt;analyze-tables&lt;/a&gt; command directly against the in-memory database created from the incoming CSV or JSON data. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/320"&gt;#320&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-insert-files"&gt;sqlite-utils insert-files&lt;/a&gt; now has the ability to insert file contents in to &lt;code&gt;TEXT&lt;/code&gt; columns in addition to the default &lt;code&gt;BLOB&lt;/code&gt;. Pass the &lt;code&gt;--text&lt;/code&gt; option or use &lt;code&gt;content_text&lt;/code&gt; as a column specifier. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/319"&gt;#319&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4&gt;evernote-to-sqlite 0.3.2&lt;/h4&gt;
&lt;p&gt;As a follow-up to &lt;a href="https://simonwillison.net/2021/Aug/22/weeknotes-dogsheep/"&gt;last week's work&lt;/a&gt; on my personal Dogsheep, I decided to re-import my Evernote notes... and found out that Evernote has changed their export mechanism in ways that broke my tool. Most concerningly their exported XML is &lt;a href="https://github.com/dogsheep/evernote-to-sqlite/issues/13"&gt;even less well-formed than it used to be&lt;/a&gt;. This &lt;a href="https://github.com/dogsheep/evernote-to-sqlite/releases/tag/0.3.2"&gt;new release&lt;/a&gt; works around that.&lt;/p&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/datasette/search-all-columns-trick"&gt;Searching all columns of a table in Datasette&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-verify"&gt;datasette-verify&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-verify/releases/tag/0.1"&gt;0.1&lt;/a&gt; - 2021-08-28
&lt;br /&gt;Verify that files can be opened by Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette"&gt;datasette&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette/releases/tag/0.59a2"&gt;0.59a2&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette/releases"&gt;97 releases total&lt;/a&gt;) - 2021-08-28
&lt;br /&gt;An open source multi-tool for exploring and publishing data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/dogsheep/evernote-to-sqlite"&gt;evernote-to-sqlite&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/dogsheep/evernote-to-sqlite/releases/tag/0.3.2"&gt;0.3.2&lt;/a&gt; - (&lt;a href="https://github.com/dogsheep/evernote-to-sqlite/releases"&gt;5 releases total&lt;/a&gt;) - 2021-08-26
&lt;br /&gt;Tools for converting Evernote content to SQLite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.17"&gt;3.17&lt;/a&gt; - (&lt;a href="https://github.com/simonw/sqlite-utils/releases"&gt;86 releases total&lt;/a&gt;) - 2021-08-24
&lt;br /&gt;Python CLI utility and library for manipulating SQLite databases&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/electron"&gt;electron&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-desktop"&gt;datasette-desktop&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="datasette"/><category term="weeknotes"/><category term="sqlite-utils"/><category term="electron"/><category term="datasette-desktop"/></entry></feed>