7991 |
datasette.io, an official project website for Datasette |
datasette-io |
<p>This week I launched <a href="https://datasette.io/">datasette.io</a> - the new official project website for Datasette.</p>
<p>Datasette's first open source release was <a href="https://simonwillison.net/2017/Nov/13/datasette/">just over three years ago</a>, but until now the official site duties have been split between the <a href="https://github.com/simonw/datasette">GitHub repository</a> and <a href="https://docs.datasette.io/">the documentation</a>.</p>
<p><img style="max-width: 100%" alt="A screenshot of datasette.io" src="https://static.simonwillison.net/static/2020/datasette-io.png" /></p>
<h4>The Baked Data architectural pattern</h4>
<p>The site itself is built on Datasette (<a href="https://github.com/simonw/datasette.io">source code here</a>). I'm using a pattern that I <a href="https://simonwillison.net/2019/Oct/28/niche-museums-kepler/">first started exploring</a> with <a href="https://www.niche-museums.com/">Niche Museums</a>: most of the site content lives in a SQLite database, and I use custom Jinja templates to implement the site's different pages.</p>
<p>This is effectively a variant of the static site generator pattern. The SQLite database is built by scripts as part of the deploy process, then deployed to Google Cloud Run as a binary asset bundled with the templates and Datasette itself.</p>
<p>I call this the <strong>Baked Data</strong> architectural pattern - with credit to Kevin Marks for <a href="https://chat.indieweb.org/dev/2020-11-15#t1605478365993400">helping me</a> coin the right term. You bake the data into the application.</p>
<p><em>Update: I wrote more about this in July 2021: <a href="https://simonwillison.net/2021/Jul/28/baked-data/">The Baked Data architectural pattern</a></em></p>
<p>It's comparable to static site generation because everything is immutable, which greatly reduces the amount of things that can go wrong - and any content changes require a fresh deploy. It's extremely easy to scale - just run more copies of the application with the bundled copy of the database. Cloud Run and other serverless providers handle that kind of scaling automatically.</p>
<p>Unlike static site generation, if a site has a thousand pages you don't need to build a thousand HTML pages in order to deploy. A single template and a SQL query that incorporates arguments from the URL can serve as many pages as there are records in the database.</p>
<h4>How the site is built</h4>
<p>You can browse the site's underlying database tables in Datasette <a href="https://datasette.io/content">here</a>.</p>
<p>The <a href="https://datasette.io/content/news">news table</a> powers the latest news on the homepage and <a href="https://datasette.io/news">/news</a>. News lives in a <a href="https://github.com/simonw/datasette.io/blob/main/news.yaml">news.yaml file</a> in the site's GitHub repository. I wrote <a href="https://gist.github.com/simonw/6a59833eee83bec1f1317c7f80406275">a script</a> to import the news that had been accumulating in <a href="https://github.com/simonw/datasette/blob/0.52.5/README.md">the 0.52 README</a> - now that news has moved to the site the README is a lot more slim!</p>
<p>At build time my <a href="https://github.com/simonw/yaml-to-sqlite">yaml-to-sqlite</a> script runs to load that news content into a database table.</p>
<p>The <a href="https://github.com/simonw/datasette.io/blob/72e3b6a470d2543dede13d51a61a22180b5c96f9/templates/index.html#L78-L85">index.html</a> template then uses the following Jinja code to output the latest news stories, using the <code>sql()</code> function from the <a href="https://github.com/simonw/datasette-template-sql">datasette-template-sql</a> Datasette plugin:</p>
<div class="highlight highlight-text-html-django"><pre><span class="pl-e">{%</span> <span class="pl-s">set</span> <span class="pl-s">ns</span> = <span class="pl-s">namespace</span>(<span class="pl-s">current_date</span>=<span class="pl-s">""</span>) <span class="pl-e">%}</span>
<span class="pl-e">{%</span> <span class="pl-k">for</span> <span class="pl-s">row</span> <span class="pl-k">in</span> <span class="pl-s">sql</span>(<span class="pl-s">"select date, body from news order by date desc limit 15"</span>, <span class="pl-s">database</span>=<span class="pl-s">"content"</span>) <span class="pl-e">%}</span>
<span class="pl-e">{%</span> <span class="pl-k">if</span> <span class="pl-s">prettydate</span>(<span class="pl-s">row</span>[<span class="pl-s">"date"</span>]) != (<span class="pl-s">ns</span>.<span class="pl-s">current_date</span> <span class="pl-k">and</span> <span class="pl-s">prettydate</span>(<span class="pl-s">ns</span>.<span class="pl-s">current_date</span>)) <span class="pl-e">%}</span>
<<span class="pl-ent">h3</span>>{{ prettydate(row["date<span class="pl-smi">"]</span>) }} <<span class="pl-ent">a</span> <span class="pl-e">href</span>=<span class="pl-s"><span class="pl-pds">"</span>/news/{{ row[<span class="pl-pds">"</span></span><span class="pl-e">date</span><span class="pl-s"><span class="pl-pds">"</span>] }}<span class="pl-pds">"</span></span> <span class="pl-e">style</span>=<span class="pl-s"><span class="pl-pds">"</span><span class="pl-s1"><span class="pl-c1"><span class="pl-c1">font-size</span></span>: <span class="pl-c1">0.8<span class="pl-k">em</span></span>; <span class="pl-c1"><span class="pl-c1">opacity</span></span>: <span class="pl-c1">0.4</span></span><span class="pl-pds">"</span></span>>#</<span class="pl-ent">a</span>></<span class="pl-ent">h3</span>>
<span class="pl-e">{%</span> <span class="pl-s">set</span> <span class="pl-s">ns</span>.<span class="pl-s">current_date</span> = <span class="pl-s">prettydate</span>(<span class="pl-s">row</span>[<span class="pl-s">"date"</span>]) <span class="pl-e">%}</span>
<span class="pl-e">{%</span> <span class="pl-k">endif</span> <span class="pl-e">%}</span>
{{ render_markdown(row["body<span class="pl-smi">"]</span>) }}
<span class="pl-e">{%</span> <span class="pl-k">endfor</span> <span class="pl-e">%}</span></pre></div>
<p><code>prettydate()</code> is a custom function I wrote in <a href="https://github.com/simonw/datasette.io/blob/353f1f940cb0cb3c38d3bdf6e345328740990702/plugins/dateformat.py">a one-off plugin</a> for the site. The <code>namespace()</code> stuff is a Jinja trick that lets me keep track of the current date heading in the loop, so I can output a new date heading only if the news item occurs on a different day from the previous one.</p>
<p><code>render_markdown()</code> is provided by the <a href="https://github.com/simonw/datasette-render-markdown">datasette-render-markdown</a> plugin.</p>
<p>I wanted permalinks for news stories, but since they don't have identifiers or titles I decided to provide a page for each day instead - for example <a href="https://datasette.io/news/2020-12-10">https://datasette.io/news/2020-12-10</a></p>
<p>These pages are implemented using <a href="https://simonwillison.net/2020/Sep/15/datasette-0-49/#path-parameters-custom-page-templates">Path parameters for custom page templates</a>, introduced in Datasette 0.49. The implementation is a single template file at <a href="https://github.com/simonw/datasette.io/blob/353f1f940cb0cb3c38d3bdf6e345328740990702/templates/pages/news/%7Byyyy%7D-%7Bmm%7D-%7Bdd%7D.html">templates/pages/news/{yyyy}-{mm}-{dd}.html</a>, the full contents of which is:</p>
<div class="highlight highlight-text-html-django"><pre><span class="pl-e">{%</span> <span class="pl-k">extends</span> <span class="pl-s">"page_base.html"</span> <span class="pl-e">%}</span>
<span class="pl-e">{%</span> <span class="pl-k">block</span> <span class="pl-s">title</span> <span class="pl-e">%}</span>Datasette News: {{ prettydate(yyyy + "-" + mm + "-" + dd) }}<span class="pl-e">{%</span> <span class="pl-k">endblock</span> <span class="pl-e">%}</span>
<span class="pl-e">{%</span> <span class="pl-k">block</span> <span class="pl-s">content</span> <span class="pl-e">%}</span>
<span class="pl-e">{%</span> <span class="pl-s">set</span> <span class="pl-s">stories</span> = <span class="pl-s">sql</span>(<span class="pl-s">"select date, body from news where date = ? order by date desc"</span>, [<span class="pl-s">yyyy</span> + <span class="pl-s">"-"</span> + <span class="pl-s">mm</span> + <span class="pl-s">"-"</span> + <span class="pl-s">dd</span>], <span class="pl-s">database</span>=<span class="pl-s">"content"</span>) <span class="pl-e">%}</span>
<span class="pl-e">{%</span> <span class="pl-k">if</span> <span class="pl-k">not</span> <span class="pl-s">stories</span> <span class="pl-e">%}</span>
{{ raise_404("News not found") }}
<span class="pl-e">{%</span> <span class="pl-k">endif</span> <span class="pl-e">%}</span>
<<span class="pl-ent">h1</span>><<span class="pl-ent">a</span> <span class="pl-e">href</span>=<span class="pl-s"><span class="pl-pds">"</span>/news<span class="pl-pds">"</span></span>>News</<span class="pl-ent">a</span>>: {{ prettydate(yyyy + "-" + mm + "-" + dd) }}</<span class="pl-ent">h1</span>>
<span class="pl-e">{%</span> <span class="pl-k">for</span> <span class="pl-s">row</span> <span class="pl-k">in</span> <span class="pl-s">stories</span> <span class="pl-e">%}</span>
{{ render_markdown(row["body<span class="pl-smi">"]</span>) }}
<span class="pl-e">{%</span> <span class="pl-k">endfor</span> <span class="pl-e">%}</span>
<span class="pl-e">{%</span> <span class="pl-k">endblock</span> <span class="pl-e">%}</span></pre></div>
<p>The crucial trick here is that, because the filename is <code>news/{yyyy}-{mm}-{dd}.html</code>, a request to <code>/news/2020-12-10</code> will render that template with the <code>yyyy</code>, <code>mm</code> and <code>dd</code> template variables set to those values from the URL.</p>
<p>It can then execute a SQL query that incorporates those values. It assigns the results to a <code>stories</code> variable, then checks that at least one story was returned - if not, it raises a 404 error.</p>
<p>See Datasette's <a href="https://docs.datasette.io/en/stable/custom_templates.html#custom-pages">custom pages documentation</a> for more details on how this all works.</p>
<p>The site also offers an <a href="https://datasette.io/content/feed.atom">Atom feed</a> of recent news. This is powered by the <a href="https://github.com/simonw/datasette-atom">datasette-atom</a> using the output of <a href="https://datasette.io/content/feed">this canned SQL query</a>, with a <code>render_markdown()</code> SQL function provided by <a href="https://github.com/simonw/datasette.io/blob/353f1f940cb0cb3c38d3bdf6e345328740990702/plugins/sql_functions.py">this site plugin</a>.</p>
<h4>The plugin directory</h4>
<p>One of the features I'm most excited about on the site is the new <a href="https://datasette.io/plugins">Datasette plugin directory</a>. Datasette has over 50 plugins now and I've been wanting a definitive directory of them for a while.</p>
<p>It's pretty basic at the moment, offering a list of plugins plus simple <code>LIKE</code> based search, but I plan to expand it a great deal in the future.</p>
<p>The fun part is where the data comes from. For a couple of years now I've been using GitHub topics to tag my plugins - I tag them with <code>datasette-plugin</code>, and the ones that I planned to feature on the site when I finally launched it were also tagged with <code>datasette-io</code>.</p>
<p>The <code>datasette.io</code> deployment process runs a script called <a href="https://github.com/simonw/datasette.io/blob/353f1f940cb0cb3c38d3bdf6e345328740990702/build_plugin_directory.py">build_plugin_directory.py</a>, which uses a GraphQL query against the GitHub search API to find all repositories belonging to me that have been tagged with those tags.</p>
<p>That GraphQL query looks like this:</p>
<div class="highlight highlight-source-graphql"><pre><span class="pl-k">query</span> {
<span class="pl-v">search</span>(<span class="pl-v">query</span>:<span class="pl-s"><span class="pl-pds">"</span>topic:datasette-io topic:datasette-plugin user:simonw<span class="pl-pds">"</span></span> <span class="pl-v">type</span>:<span class="pl-c1">REPOSITORY</span>, <span class="pl-v">first</span>:<span class="pl-c1">100</span>) {
<span class="pl-v">repositoryCount</span>
<span class="pl-v">nodes</span> {
<span class="pl-k">...</span> <span class="pl-k">on</span> <span class="pl-c1">Repository</span> {
<span class="pl-v">id</span>
<span class="pl-v">nameWithOwner</span>
<span class="pl-v">openGraphImageUrl</span>
<span class="pl-v">usesCustomOpenGraphImage</span>
<span class="pl-v">repositoryTopics</span>(<span class="pl-v">first</span>:<span class="pl-c1">100</span>) {
<span class="pl-v">totalCount</span>
<span class="pl-v">nodes</span> {
<span class="pl-v">topic</span> {
<span class="pl-v">name</span>
}
}
}
<span class="pl-s">openIssueCount</span>: <span class="pl-v">issues</span>(<span class="pl-v">states</span>:[<span class="pl-c1">OPEN</span>]) {
<span class="pl-v">totalCount</span>
}
<span class="pl-s">closedIssueCount</span>: <span class="pl-v">issues</span>(<span class="pl-v">states</span>:[<span class="pl-c1">CLOSED</span>]) {
<span class="pl-v">totalCount</span>
}
<span class="pl-v">releases</span>(<span class="pl-v">last</span>: <span class="pl-c1">1</span>) {
<span class="pl-v">totalCount</span>
<span class="pl-v">nodes</span> {
<span class="pl-v">tagName</span>
}
}
}
}
}
}</pre></div>
<p>It fetches the name of each repository, the <code>openGraphImageUrl</code> (which doesn't appear to be included in the regular GitHub REST API), the number of open and closed issues and details of the most recent release.</p>
<p>The script has access to a copy of the current site database, which is downloaded on each deploy by the build script. It uses this to check if any of the repositories have new releases that haven't previously been seen by the script.</p>
<p>Then it runs the <code>github-to-sqlite releases</code> command (part of <a href="https://github.com/dogsheep/github-to-sqlite">github-to-sqlite</a>) to fetch details of those new releases.</p>
<p>The end result is a database of repositories and releases for all of my tagged plugins. The plugin directory is then built against a <a href="https://datasette.io/content/plugins">custom SQL view</a>.</p>
<h4>Other site content</h4>
<p>The rest of the site content is mainly static template files. I use the <code>render_markdown()</code> function inline in some of them so I can author in Markdown rather than HTML - here's <a href="https://github.com/simonw/datasette.io/blob/main/templates/pages/examples.html">the template</a> for the <a href="https://datasette.io/examples">/examples page</a>. The various <a href="https://datasette.io/for">Use cases for Datasette</a> pages are likewise built as static templates.</p>
<h4 id="sqlite-utils-analyze-tables">Also this week: sqlite-utils analyze-tables</h4>
<p>My other big project this week has involved building out a Datasette instance for a client. I'm working with over 5,000,000 rows of CSV data for this, which has been a great opportunity to push the limits of some of my tools.</p>
<p>Any time I'm working with new data I like to get a feel for its general shape. Having imported 5,000,000 rows with dozens of columns into a database, what can I learn about the columns beyond just browsing them in Datasette?</p>
<p><code>sqlite-utils analyze-tables</code> (<a href="https://sqlite-utils.readthedocs.io/en/stable/cli.html#analyzing-tables">documented here</a>) is my new tool for doing just that. It loops through every table and every column in the database, and for each column it calculates statistics that include:</p>
<ul>
<li>The total number of distinct values</li>
<li>The total number of null or blank values</li>
<li>For non-distinct columns, the 10 most common and 10 least common values</li>
</ul>
<p>It can output those to the terminal, or if you add the <code>--save</code> option it will also save them to a SQLite table called <code>_analyze_tables_</code> - here's <a href="https://github-to-sqlite.dogsheep.net/github/_analyze_tables_">that table</a> for my github-to-sqlite demo instance.</p>
<p>I can then use the output of the tool to figure out which columns might be a primary key, or which ones warrant being extracted out into a separate lookup table using <a href="https://simonwillison.net/2020/Sep/23/sqlite-utils-extract/">sqlite-utils extract</a>.</p>
<p>I expect I'll be expanding this feature a lot in the future, but I'm already finding it to be really helpful.</p>
<h4>Datasette 0.53</h4>
<p>I pushed out a small feature release of Datasette to accompany the new project website. Quoting <a href="https://docs.datasette.io/en/stable/changelog.html#v0-53">the release notes</a>:</p>
<blockquote>
<ul>
<li>New <code>?column__arraynotcontains=</code> table filter. (<a href="https://github.com/simonw/datasette/issues/1132">#1132</a>)</li>
<li>
<code>datasette serve</code> has a new <code>--create</code> option, which will create blank database files if they do not already exist rather than exiting with an error. (<a href="https://github.com/simonw/datasette/issues/1135">#1135</a>)</li>
<li>New <code>?_header=off</code> option for CSV export which omits the CSV header row, <a href="https://docs.datasette.io/en/stable/csv_export.html#csv-export-url-parameters">documented here</a>. (<a href="https://github.com/simonw/datasette/issues/1133">#1133</a>)</li>
<li>"Powered by Datasette" link in the footer now links to <a href="https://datasette.io/">https://datasette.io/</a>. (<a href="https://github.com/simonw/datasette/issues/1138">#1138</a>)</li>
<li>Project news no longer lives in the README - it can now be found at <a href="https://datasette.io/news">https://datasette.io/news</a>. (<a href="https://github.com/simonw/datasette/issues/1137">#1137</a>)</li>
</ul>
</blockquote>
<h4>Office hours</h4>
<p>I had my first round of <a href="https://calendly.com/swillison/datasette-office-hours">Datasette office hours</a> on Friday - 20 minute video chats with anyone who wants to talk to me about the project. I had five great conversations - it's hard to overstate how thrilling it is to talk to people who are using Datasette to solve problems. If you're an open source maintainer I can thoroughly recommend giving this format a try.</p>
<h4>Releases this week</h4>
<ul>
<li>
<strong><a href="https://github.com/simonw/datasette-publish-fly">datasette-publish-fly</a></strong>: <a href="https://github.com/simonw/datasette-publish-fly/releases/tag/1.0.1">1.0.1</a> - 2020-12-12<br />
Datasette plugin for publishing data using Fly</li>
<li>
<strong><a href="https://github.com/simonw/datasette-auth-passwords">datasette-auth-passwords</a></strong>: <a href="https://github.com/simonw/datasette-auth-passwords/releases/tag/0.3.3">0.3.3</a> - 2020-12-11<br />
Datasette plugin for authentication using passwords</li>
<li>
<strong><a href="https://github.com/simonw/datasette">datasette</a></strong>: <a href="https://github.com/simonw/datasette/releases/tag/0.53">0.53</a> - - 2020-12-11<br />
An open source multi-tool for exploring and publishing data</li>
<li>
<strong><a href="https://github.com/simonw/datasette-column-inspect">datasette-column-inspect</a></strong>: <a href="https://github.com/simonw/datasette-column-inspect/releases/tag/0.2a">0.2a</a> - 2020-12-09<br />
Experimental plugin that adds a column inspector</li>
<li>
<strong><a href="https://github.com/simonw/datasette-pretty-json">datasette-pretty-json</a></strong>: <a href="https://github.com/simonw/datasette-pretty-json/releases/tag/0.2.1">0.2.1</a> - 2020-12-09<br />
Datasette plugin that pretty-prints any column values that are valid JSON objects or arrays</li>
<li>
<strong><a href="https://github.com/simonw/yaml-to-sqlite">yaml-to-sqlite</a></strong>: <a href="https://github.com/simonw/yaml-to-sqlite/releases/tag/0.3.1">0.3.1</a> - 2020-12-07<br />
Utility for converting YAML files to SQLite</li>
<li>
<strong><a href="https://github.com/simonw/datasette-seaborn">datasette-seaborn</a></strong>: <a href="https://github.com/simonw/datasette-seaborn/releases/tag/0.2a0">0.2a0</a> - 2020-12-07<br />
Statistical visualizations for Datasette using Seaborn</li>
</ul>
<h4>TIL this week</h4>
<ul>
<li><a href="https://til.simonwillison.net/readthedocs/custom-sphinx-templates">Using custom Sphinx templates on Read the Docs</a></li>
<li><a href="https://til.simonwillison.net/python/style-yaml-dump">Controlling the style of dumped YAML using PyYAML</a></li>
<li><a href="https://til.simonwillison.net/bash/escaping-sql-for-curl-to-datasette">Escaping a SQL query to use with curl and Datasette</a></li>
<li><a href="https://til.simonwillison.net/bash/skip-csv-rows-with-odd-numbers">Skipping CSV rows with odd numbers of quotes using ripgrep</a></li>
</ul> |
2020-12-13 08:34:44+00:00 |
{} |
'-07':1714C,1729C '-09':1675C,1690C '-11':1644C,1655C '-12':1628C,1629C,1643C,1654C,1674C,1689C,1713C,1728C '/.':1530C '/a':461C,654C '/examples':1175C '/h1':659C '/h3':462C '/news':322C,450C,652C '/news.':1548C '/news/2020-12-10':573C,688C '0.2':1671C,1725C '0.2.1':1687C '0.3.1':1711C '0.3.3':1641C '0.4':460C '0.49':588C '0.52':349C '0.53':1450C,1652C '0.8':457C '000':1221C,1222C,1265C,1266C '1':1001C '1.0.1':1626C '10':1344C,1348C '100':973C,984C '1132':1475C '1133':1517C '1135':1501C '1137':1549C '1138':1531C '15':431C '20':1563C '2020':1627C,1642C,1653C,1673C,1688C,1712C,1727C '2021':176C '404':644C,741C '5':1220C,1264C '50':818C 'a':67C,84C,103C,131C,209C,249C,252C,261C,269C,273C,326C,337C,362C,382C,448C,477C,483C,495C,515C,526C,563C,592C,650C,685C,712C,724C,740C,784C,826C,832C,842C,857C,873C,923C,929C,1046C,1112C,1131C,1209C,1213C,1232C,1256C,1273C,1372C,1406C,1417C,1434C,1454C,1479C,1617C,1672C,1680C,1756C 'a0':1726C 'about':172C,805C,1279C,1575C 'access':1044C 'accompany':1461C 'accumulating':346C 'add':1362C 'adds':1679C 'against':932C,1130C 'ago':31C 'all':755C,939C,1119C 'already':1442C,1493C 'also':759C,912C,1190C,1368C 'amount':197C 'an':2A,761C,1499C,1606C,1656C 'analyze':1197C,1292C,1376C 'analyze-tables':1196C,1291C 'and':46C,87C,138C,204C,234C,272C,321C,440C,697C,821C,895C,1031C,1034C,1116C,1309C,1315C,1347C,1664C,1763C 'any':205C,1069C,1244C,1697C 'anyone':1568C 'api':936C,1026C 'appear':1017C 'application':167C,224C 'architectural':52C,147C,180C 'are':289C,576C,1184C,1597C,1701C 'arguments':278C 'arraynotcontains':1472C 'arrays':1706C 'as':118C,130C,284C,287C,1187C 'asset':133C 'assigns':720C 'at':366C,596C,730C,838C,1545C 'atom':762C,774C 'auth':1639C 'authentication':1648C 'author':1163C 'automatically':243C 'bake':162C 'baked':50C,145C,178C 'bakeddata':1779B 'based':849C 'basic':837C 'be':1019C,1405C,1430C,1446C,1543C 'because':189C,676C 'been':40C,345C,824C,880C,946C,1080C,1231C 'being':1413C 'belonging':941C 'between':42C 'beyond':1282C 'big':1201C 'binary':132C 'blank':1336C,1486C 'block':610C,619C 'body':423C,473C,626C,667C 'browse':301C 'browsing':1284C 'build':260C,367C,1061C 'build_plugin_directory.py':926C 'building':1207C 'built':58C,115C,298C,1129C,1186C 'bundled':134C,227C 'but':32C,550C,851C,1439C 'by':116C,427C,537C,632C,770C,790C,1059C,1082C,1519C 'calculates':1320C 'call':142C 'called':925C,1375C 'can':201C,282C,300C,513C,709C,1162C,1276C,1353C,1391C,1541C,1611C 'canned':780C 'cases':1180C 'changes':207C 'chats':1566C 'check':1067C 'checks':728C 'client':1214C 'closed':997C,1032C 'closedissuecount':994C 'cloud':128C,232C 'code':62C,393C 'coin':157C 'column':1311C,1318C,1471C,1669C,1681C,1698C 'columns':1271C,1281C,1342C,1403C 'comes':870C 'command':1094C 'common':1346C,1350C 'comparable':184C 'content':81C,206C,380C,433C,620C,639C,1137C,1143C 'contents':604C 'controlling':1747C 'conversations':1582C 'converting':1717C 'copies':221C 'copy':228C,1047C 'couple':874C 'create':1481C,1485C 'credit':150C 'crucial':671C 'csv':1225C,1507C,1512C,1766C 'curl':1762C 'current':415C,505C,1050C 'custom':90C,478C,582C,746C,1132C,1740C 'data':51C,146C,164C,179C,869C,1226C,1251C,1634C,1666C 'database':86C,113C,231C,293C,306C,383C,432C,638C,1052C,1113C,1274C,1314C,1487C 'datasette':7A,19C,20C,60C,139C,309C,407C,410C,540C,587C,612C,744C,773C,812C,815C,893C,916C,962C,966C,1182C,1210C,1287C,1449C,1459C,1476C,1520C,1558C,1599C,1623C,1630C,1638C,1645C,1651C,1668C,1684C,1691C,1723C,1733C,1764C,1776B 'datasette-atom':772C 'datasette-auth-passwords':1637C 'datasette-column-inspect':1667C 'datasette-io':915C,961C 'datasette-plugin':892C,965C 'datasette-pretty-json':1683C 'datasette-publish-fly':1622C 'datasette-render-markdown':539C 'datasette-seaborn':1722C 'datasette-template-sql':406C 'datasette.io':1A,12C,572C,919C,1529C,1547C 'datasette.io/.':1528C 'datasette.io/news.':1546C 'datasette.io/news/2020-12-10':571C 'date':416C,422C,428C,437C,439C,443C,447C,452C,465C,468C,506C,517C,625C,630C,633C 'day':528C,567C 'dd':600C,617C,637C,658C,683C,698C 'deal':859C 'decided':560C 'definitive':827C 'demo':1388C 'deploy':122C,211C,268C,1058C 'deployed':125C 'deployment':920C 'desc':429C,634C 'details':751C,1035C,1103C 'different':98C,527C 'directory':796C,814C,828C,1126C 'distinct':1328C,1341C 'do':1491C 'docs':1746C 'documentation':48C,748C 'documented':1294C,1515C 'doesn':1015C 'doing':1301C 'don':256C,553C 'downloaded':1055C 'dozens':1269C 'dumped':1751C 'duties':38C 'each':566C,1010C,1057C,1317C 'easy':215C 'effectively':102C 'em':458C 'end':1109C 'endblock':618C,669C 'endfor':474C,668C 'endif':469C,648C 'error':742C,1500C 'escaping':1755C 'every':1307C,1310C 'everything':190C 'example':570C 'excited':804C 'execute':711C 'exist':1494C 'exiting':1497C 'expand':855C 'expanding':1431C 'expect':1427C 'experimental':1676C 'exploring':73C,1663C 'export':1508C 'extends':608C 'extract':1425C 'extracted':1414C 'extremely':214C 'feature':902C,1433C,1456C 'features':800C 'feed':763C 'feel':1257C 'fetch':1102C 'fetches':1006C 'figure':1400C 'file':328C,595C 'filename':678C 'files':1148C,1488C,1719C 'filter':1474C 'finally':908C 'find':938C 'finding':1443C 'first':22C,71C,972C,983C,1555C 'five':1580C 'fly':1625C,1636C 'following':391C 'font':455C 'font-size':454C 'footer':1524C 'for':6A,18C,154C,417C,488C,547C,565C,569C,581C,660C,749C,831C,872C,1118C,1173C,1181C,1212C,1227C,1258C,1300C,1316C,1338C,1382C,1506C,1632C,1647C,1662C,1716C,1732C 'format':1616C 'found':647C,1544C 'fresh':210C 'friday':1562C 'from':279C,404C,424C,529C,627C,705C,871C 'full':603C 'fun':864C 'function':403C,479C,788C,1154C 'future':862C,1438C 'general':1260C 'generation':188C,247C 'generator':109C 'get':1255C 'github':44C,333C,882C,934C,1024C,1090C,1098C,1385C 'github-to-sqlite':1089C,1097C,1384C 'giving':1614C 'go':202C 'google':127C 'graphql':930C,952C 'great':858C,1233C,1581C 'greatly':194C 'h1':649C 'h3':444C 'had':344C,1553C,1579C 'handle':238C 'hard':1585C 'has':251C,354C,816C,1043C,1205C,1230C,1478C 'have':39C,555C,945C,1073C 'haven':1077C 'having':1262C 'header':1503C,1513C 'heading':507C,518C 'helpful':1448C 'helping':155C 'here':63C,310C,673C,1169C,1295C,1378C,1516C 'homepage':320C 'hours':1551C,1560C 'how':294C,753C,1588C 'href':449C,651C 'html':263C,601C,684C,1168C 'i':10C,64C,70C,88C,141C,169C,335C,480C,512C,544C,559C,801C,822C,852C,878C,888C,899C,907C,1149C,1161C,1215C,1246C,1252C,1277C,1390C,1426C,1428C,1440C,1451C,1552C,1578C,1610C 'id':978C 'identifiers':556C 'if':248C,434C,520C,640C,736C,1068C,1360C,1489C,1603C 'immutable':192C 'implement':94C 'implementation':590C 'implemented':577C 'import':340C 'imported':1263C 'in':83C,174C,265C,291C,308C,325C,329C,347C,419C,482C,508C,586C,662C,860C,1021C,1156C,1164C,1286C,1312C,1436C,1522C,1537C 'include':1323C 'included':1020C 'incorporates':277C,716C 'index.html':386C 'inline':1155C 'inspect':1670C 'inspector':1682C 'instance':1211C,1389C 'instead':568C 'into':165C,381C,1272C,1416C 'introduced':585C 'involved':1206C 'io':917C,963C 'is':57C,101C,114C,191C,297C,361C,476C,494C,535C,591C,607C,674C,679C,768C,809C,866C,1054C,1111C,1127C,1144C,1296C,1591C 'issues':990C,995C,1033C 'it':182C,212C,708C,719C,738C,834C,856C,910C,1005C,1063C,1086C,1304C,1319C,1352C,1366C,1444C,1540C,1583C,1590C 'item':523C 'its':1259C 'itself':56C,140C 'jinja':91C,392C,496C 'json':1686C,1703C 'july':175C 'just':27C,218C,1283C,1302C 'keep':501C 'kevin':152C 'key':1408C 'kind':240C 'last':1000C 'latest':316C,397C 'launched':11C,909C 'learn':1278C 'least':731C,1349C 'lets':499C 'like':848C,955C,1253C 'likewise':1185C 'limit':430C 'limits':1238C 'link':1521C 'links':1526C 'list':843C 'lives':82C,324C,1536C 'll':1429C 'load':377C 'longer':1535C 'looks':954C 'lookup':1419C 'loop':510C 'loops':1305C 'lot':363C,1435C 'm':65C,802C,1216C,1247C,1441C 'mainly':1145C 'maintainer':1609C 'many':285C 'markdown':471C,534C,542C,665C,786C,1153C,1165C 'marks':153C 'me':156C,500C,943C,1574C 'might':1404C 'minute':1564C 'mm':599C,616C,636C,657C,682C,696C 'moment':840C 'more':171C,220C,364C,750C 'most':77C,803C,1038C,1345C 'moved':355C 'multi':1660C 'multi-tool':1659C 'museums':76C 'my':369C,886C,1121C,1199C,1242C,1297C,1383C,1554C 'name':988C,1008C 'namespace':414C,492C 'namewithowner':979C 'need':258C 'new':14C,516C,811C,1074C,1106C,1250C,1298C,1463C,1470C,1480C,1502C 'news':312C,317C,323C,342C,353C,379C,398C,425C,522C,548C,613C,628C,645C,653C,680C,766C,1533C 'news.yaml':327C 'niche':75C 'no':1534C 'nodes':975C,986C,1003C 'non':1340C 'non-distinct':1339C 'not':641C,646C,737C,1492C 'notes':1469C 'now':34C,351C,820C,877C,1525C,1542C 'ns':413C 'ns.current':438C,442C,464C 'null':1334C 'number':1028C,1326C,1332C 'numbers':1770C 'objects':1704C 'occurs':524C 'odd':1769C 'of':78C,105C,120C,198C,222C,229C,241C,503C,605C,764C,778C,798C,829C,844C,875C,1009C,1029C,1036C,1048C,1070C,1096C,1104C,1114C,1120C,1140C,1158C,1224C,1239C,1241C,1270C,1327C,1333C,1396C,1458C,1557C,1750C,1771C 'off':486C,1504C 'offering':841C 'offers':760C 'office':1550C,1559C 'official':3A,15C,36C 'omits':1510C 'on':59C,318C,525C,752C,806C,903C,976C,1056C,1561C,1743C 'one':485C,532C,732C,797C 'one-off':484C 'ones':897C,1411C 'only':519C 'opacity':459C 'open':23C,992C,1030C,1607C,1657C 'opengraphimageurl':980C,1013C 'openissuecount':989C 'opportunity':1234C 'option':1365C,1482C,1505C 'or':557C,1335C,1359C,1409C,1705C 'order':266C,426C,631C 'other':235C,1135C,1200C 'out':1208C,1401C,1415C,1453C 'output':395C,514C,777C,1354C,1395C 'over':28C,817C,1219C 'overstate':1587C 'page':564C,583C,1176C 'page_base.html':609C 'pages':99C,254C,264C,286C,575C,747C,1183C 'parameters':580C 'part':119C,865C,1095C 'passwords':1640C,1650C 'path':579C 'pattern':53C,68C,110C,148C,181C 'people':1595C 'permalinks':546C 'plan':853C 'planned':900C 'plugin':411C,487C,543C,793C,795C,813C,894C,967C,1125C,1631C,1646C,1677C,1692C 'plugins':819C,845C,887C,1123C 'plus':846C 'powered':769C,1518C 'powers':314C 'pretty':836C,1685C,1695C 'pretty-prints':1694C 'prettydate':435C,441C,445C,466C,475C,614C,655C 'previous':531C 'previously':1079C 'primary':1407C 'prints':1696C 'problems':1602C 'process':123C,921C 'project':4A,16C,1202C,1464C,1532C,1577C 'projects':1775B 'provide':562C 'provided':536C,789C 'providers':237C 'publish':1624C 'publishing':1633C,1665C 'push':1236C 'pushed':1452C 'pyyaml':1754C 'query':275C,714C,782C,931C,953C,957C,959C,1758C 'quotes':1772C 'quoting':1466C 'raise':643C 'raises':739C 'rather':1166C,1495C 're':1605C 'read':1744C 'readme':350C,360C,1539C 'really':1447C 'recent':765C,1039C 'recommend':1613C 'records':290C 'reduces':195C 'regular':1023C 'release':25C,1040C,1457C,1468C 'releases':999C,1075C,1093C,1107C,1117C,1619C 'render':470C,533C,541C,664C,690C,785C,1152C 'repositories':940C,1072C,1115C 'repository':45C,334C,971C,977C,1011C 'repositorycount':974C 'repositorytopics':982C 'request':686C 'require':208C 'rest':1025C,1139C 'result':1110C 'results':722C 'returned':735C 'right':159C 'ripgrep':1774C 'round':1556C 'row':418C,436C,446C,451C,467C,472C,661C,666C,1514C 'rows':1223C,1267C,1767C 'run':129C,219C,233C 'runs':375C,922C,1087C 's':21C,97C,183C,213C,304C,332C,745C,835C,1170C,1379C,1584C 'save':1364C,1369C 'scale':217C 'scaling':242C 'script':338C,374C,924C,1042C,1062C,1084C 'scripts':117C 'seaborn':1724C,1735C 'search':850C,935C,958C 'see':743C 'seen':1081C 'select':421C,624C 'separate':1418C 'serve':283C,1477C 'serverless':236C 'set':412C,463C,621C,701C 'shape':1261C 'simonw':969C 'simple':847C 'since':551C 'single':270C,593C 'site':37C,55C,80C,96C,108C,187C,246C,250C,296C,303C,331C,358C,490C,758C,792C,808C,905C,1051C,1136C,1142C 'size':456C 'skipping':1765C 'slim':365C 'small':1455C 'so':511C,1160C 'solve':1601C 'some':1157C,1240C 'source':24C,61C,1608C,1658C 'sphinx':1741C 'split':41C 'sql':274C,402C,409C,420C,623C,713C,781C,787C,1133C,1757C 'sqlite':85C,112C,373C,1092C,1100C,1194C,1289C,1373C,1387C,1423C,1710C,1721C 'sqlite-utils':1193C,1288C,1422C 'sqliteutils':1778B 'started':72C 'states':991C,996C 'static':107C,186C,245C,1146C,1188C 'statistical':1730C 'statistics':1321C 'stories':399C,549C,622C,642C,663C,725C 'story':733C 'stuff':493C 'style':453C,1749C 't':257C,554C,1016C,1078C 'table':313C,384C,1308C,1374C,1381C,1420C,1473C 'tables':307C,1198C,1293C,1377C 'tag':885C,889C 'tagged':913C,947C,1122C 'tagname':1004C 'tags':950C 'talk':1572C,1593C 'template':271C,387C,408C,594C,692C,699C,1147C,1172C 'templates':92C,137C,584C,1189C,1742C 'templates/pages/news':597C 'term':160C 'terminal':1358C 'than':1167C,1496C 'that':69C,200C,239C,276C,343C,352C,378C,498C,675C,691C,715C,729C,898C,944C,951C,1076C,1303C,1322C,1380C,1678C,1693C,1700C 'the':13C,35C,43C,47C,49C,54C,79C,95C,106C,111C,121C,136C,144C,158C,163C,166C,177C,196C,223C,226C,230C,280C,292C,295C,302C,311C,315C,319C,330C,341C,348C,357C,359C,385C,390C,396C,401C,405C,489C,491C,504C,509C,521C,530C,538C,589C,602C,670C,677C,694C,706C,721C,757C,771C,776C,794C,799C,807C,810C,839C,861C,863C,868C,896C,904C,918C,933C,1007C,1012C,1022C,1027C,1037C,1041C,1049C,1060C,1071C,1083C,1088C,1108C,1124C,1138C,1141C,1151C,1171C,1174C,1177C,1237C,1280C,1313C,1324C,1330C,1343C,1357C,1363C,1394C,1397C,1437C,1462C,1467C,1511C,1523C,1538C,1576C,1745C,1748C 'them':830C,890C,1159C,1285C,1370C 'then':124C,388C,710C,727C,1085C,1128C,1392C 'there':288C 'these':574C 'they':552C,1490C 'things':199C 'this':8C,100C,143C,173C,754C,767C,779C,791C,956C,1065C,1191C,1203C,1228C,1432C,1615C,1620C,1737C 'thoroughly':1612C 'those':703C,717C,949C,1105C,1355C 'thousand':253C,262C 'three':29C 'thrilling':1589C 'through':1306C 'til':1736C 'time':368C,1245C 'title':611C 'titles':558C 'to':93C,126C,151C,185C,216C,259C,267C,339C,356C,372C,376C,394C,561C,687C,702C,723C,854C,884C,901C,937C,942C,1018C,1045C,1066C,1091C,1099C,1101C,1235C,1254C,1356C,1371C,1386C,1399C,1445C,1460C,1527C,1571C,1573C,1586C,1592C,1594C,1600C,1709C,1720C,1759C 'tool':1299C,1398C,1661C 'tools':1243C 'topic':960C,964C,987C 'topics':883C 'total':1325C,1331C 'totalcount':985C,993C,998C,1002C 'track':502C 'trick':497C,672C 'try':1618C 'type':970C 'underlying':305C 'unlike':244C 'until':33C 'update':168C 'url':281C,707C 'use':89C,1150C,1179C,1393C,1760C 'user':968C 'uses':389C,928C,1064C 'usescustomopengraphimage':981C 'using':66C,400C,578C,775C,881C,1421C,1598C,1635C,1649C,1734C,1739C,1753C,1773C 'utility':1715C 'utils':1195C,1290C,1424C 'valid':1702C 'values':704C,718C,1329C,1337C,1351C,1699C 'variable':726C 'variables':700C 'variant':104C 'various':1178C 've':823C,879C 'video':1565C 'view':1134C 'visualizations':1731C 'wanted':545C 'wanting':825C 'wants':1570C 'warrant':1412C 'was':26C,734C 'website':5A,17C,1465C 'week':9C,1192C,1204C,1621C,1738C 'weeknotes':1777B 'were':911C 'what':1275C 'when':906C 'where':629C,867C 'which':193C,606C,927C,1014C,1053C,1229C,1402C,1410C,1483C,1509C 'while':833C 'who':1569C,1596C 'will':689C,1367C,1484C 'with':74C,135C,149C,225C,693C,783C,891C,914C,948C,1218C,1249C,1268C,1498C,1567C,1761C,1768C 'working':1217C,1248C 'works':756C 'wrong':203C 'wrote':170C,336C,481C 'yaml':371C,1708C,1718C,1752C 'yaml-to-sqlite':370C,1707C 'years':30C,876C 'you':161C,255C,299C,1361C,1604C 'yyyy':598C,615C,635C,656C,681C,695C |
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Weeknotes: <a href="https://t.co/BNfSWtJruW">https://t.co/BNfSWtJruW</a>, an official project website for Datasette <a href="https://t.co/R5Clj8FUxf">https://t.co/R5Clj8FUxf</a></p>— Simon Willison (@simonw) <a href="https://twitter.com/simonw/status/1338040814828617728?ref_src=twsrc%5Etfw">December 13, 2020</a></blockquote> |
- null - |
|
https://static.simonwillison.net/static/2020/datasette-io.png |
- null - |
- null - |
False |
- null - |