<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: zeit-now</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/zeit-now.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2021-03-21T05:50:25+00:00</updated><author><name>Simon Willison</name></author><entry><title>Weeknotes: django-sql-dashboard widgets</title><link href="https://simonwillison.net/2021/Mar/21/django-sql-dashboard-widgets/#atom-tag" rel="alternate"/><published>2021-03-21T05:50:25+00:00</published><updated>2021-03-21T05:50:25+00:00</updated><id>https://simonwillison.net/2021/Mar/21/django-sql-dashboard-widgets/#atom-tag</id><summary type="html">
    &lt;p&gt;A few small releases this week, for &lt;code&gt;django-sql-dashboard&lt;/code&gt;, &lt;code&gt;datasette-auth-passwords&lt;/code&gt; and &lt;code&gt;datasette-publish-vercel&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;django-sql-dashboard widgets and permissions&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/django-sql-dashboard"&gt;django-sql-dashboard&lt;/a&gt;, my subset-of-Datasette-for-Django-and-PostgreSQL continues to come together.&lt;/p&gt;
&lt;p&gt;New this week: widgets and permissions.&lt;/p&gt;
&lt;p&gt;To recap: this Django app borrows some ideas from Datasette: it encourages you to create a read-only PostgreSQL user and grant authenticated users the ability to run one or more raw SQL queries directly against your database.&lt;/p&gt;
&lt;p&gt;You can execute more than one SQL query and combine them into a saved dashboard, which will then show multiple tables containing the results.&lt;/p&gt;
&lt;p&gt;This week I added support for dashboard widgets. You can construct SQL queries to return specific column patterns which will then be rendered on the page in different ways.&lt;/p&gt;
&lt;p&gt;There are four widgets at the moment: "big number", bar chart, HTML and Markdown.&lt;/p&gt;
&lt;p&gt;Big number is the simplest: define a SQL query that returns two columns called &lt;code&gt;label&lt;/code&gt; and &lt;code&gt;big_number&lt;/code&gt; and the dashboard will display that result as a big number:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Entries&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; label, &lt;span class="pl-c1"&gt;count&lt;/span&gt;(&lt;span class="pl-k"&gt;*&lt;/span&gt;) &lt;span class="pl-k"&gt;as&lt;/span&gt; big_number &lt;span class="pl-k"&gt;from&lt;/span&gt; blog_entry;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img alt="Entries: 2804 - an example of a big number display" src="https://static.simonwillison.net/static/2021/dashboard-big-number.png" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Bar chart is more sophisticated: return columns named &lt;code&gt;bar_label&lt;/code&gt; and &lt;code&gt;bar_quantity&lt;/code&gt; to display a bar chart of the results:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt;
  to_char(date_trunc(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;month&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created), &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;YYYY-MM&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;) &lt;span class="pl-k"&gt;as&lt;/span&gt; bar_label,
  &lt;span class="pl-c1"&gt;count&lt;/span&gt;(&lt;span class="pl-k"&gt;*&lt;/span&gt;) &lt;span class="pl-k"&gt;as&lt;/span&gt; bar_quantity
&lt;span class="pl-k"&gt;from&lt;/span&gt;
  blog_entry
&lt;span class="pl-k"&gt;group by&lt;/span&gt;
  bar_label
&lt;span class="pl-k"&gt;order by&lt;/span&gt;
  &lt;span class="pl-c1"&gt;count&lt;/span&gt;(&lt;span class="pl-k"&gt;*&lt;/span&gt;) &lt;span class="pl-k"&gt;desc&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img alt="A bar chart showing the result of that query" src="https://static.simonwillison.net/static/2021/dashboard-bar-chart.png" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;HTML and Markdown are simpler: they display the rendered HTML or Markdown, after filtering it through the Bleach library to strip any harmful elements or scripts.&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt;
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;## Ten most recent blogmarks (of &lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; 
  &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-c1"&gt;count&lt;/span&gt;(&lt;span class="pl-k"&gt;*&lt;/span&gt;) &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt; total)&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;as&lt;/span&gt; markdown &lt;span class="pl-k"&gt;from&lt;/span&gt; blog_blogmark;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I'm running the dashboard application on this blog, and I've set up &lt;a href="https://simonwillison.net/dashboard/example-dashboard/"&gt;an example dashboard&lt;/a&gt; here that illustrates the different types of widget.&lt;/p&gt;
&lt;p&gt;&lt;img alt="An example dashboard with several different widgets" src="https://static.simonwillison.net/static/2021/dashboard-example.png" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Defining custom widgets is easy: take the column names you would like to respond to, sort them alphabetically, join them with hyphens and create a custom widget in a template file with that name.&lt;/p&gt;
&lt;p&gt;So if you wanted to build a widget that looks for &lt;code&gt;label&lt;/code&gt; and &lt;code&gt;geojson&lt;/code&gt; columns and renders that data on a &lt;a href="https://leafletjs.com/"&gt;Leaflet map&lt;/a&gt;, you would create a &lt;code&gt;geojson-label.html&lt;/code&gt; template and drop it into your Django &lt;code&gt;templates/django-sql-dashboard/widgets&lt;/code&gt; folder. See &lt;a href="https://django-sql-dashboard.readthedocs.io/en/latest/widgets.html#custom-widgets"&gt;the custom widgets documentation&lt;/a&gt; for details.&lt;/p&gt;
&lt;p&gt;Which reminds me: I decided a README wasn't quite enough space for documentation here, so I started a &lt;a href="https://django-sql-dashboard.readthedocs.io/"&gt;Read The Docs documentation site&lt;/a&gt; for the project.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.datasette.io/"&gt;Datasette&lt;/a&gt; and &lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt; both use &lt;a href="https://www.sphinx-doc.org/"&gt;Sphinx&lt;/a&gt; and &lt;a href="https://docutils.sourceforge.io/rst.html"&gt;reStructuredText&lt;/a&gt; for their documentation.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;django-sql-dashboard&lt;/code&gt; I've decided to try out Sphinx and Markdown instead, using &lt;a href="https://myst-parser.readthedocs.io/"&gt;MyST&lt;/a&gt; - a Markdown flavour and parser for Sphinx.&lt;/p&gt;
&lt;p&gt;I picked this because I want to add inline help to &lt;code&gt;django-sql-dashboard&lt;/code&gt;, and since it ships with Markdown as a dependency already (to power the Markdown widget) my hope is that using Markdown for the documentation will allow me to ship some of the user-facing docs as part of the application itself. But it's also a fun excuse to try out MyST, which so far is working exactly as advertised.&lt;/p&gt;
&lt;p&gt;I've seen people in the past avoid Sphinx entirely because they preferred Markdown to reStructuredText, so MyST feels like an important addition to the Python documentation ecosystem.&lt;/p&gt;
&lt;h4&gt;HTTP Basic authentication&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://datasette.io/plugins/datasette-auth-passwords"&gt;datasette-auth-passwords&lt;/a&gt; implements password-based authentication to Datasette. The plugin defaults to providing a username and password login form which sets a signed cookie identifying the current user.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/datasette-auth-passwords/releases/tag/0.4"&gt;Version 0.4&lt;/a&gt; introduces &lt;a href="https://github.com/simonw/datasette-auth-passwords/issues/15"&gt;optional support&lt;/a&gt; for HTTP Basic authentication instead - where the user's browser handles the authentication prompt.&lt;/p&gt;
&lt;p&gt;Basic auth has some disadvantages - most notably that it doesn't support logout without the user entirely closing down their browser. But it's useful for a number of reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It's easy to protect every resource on a website with it - including static assets. Adding &lt;code&gt;"http_basic_auth": true&lt;/code&gt; to your plugin configuration adds this protection, covering all of Datasette's resources.&lt;/li&gt;
&lt;li&gt;It's much easier to authenticate with from automated scripts. &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;roquests&lt;/code&gt; and &lt;code&gt;httpx&lt;/code&gt; all have simple built-in support for passing basic authentication usernames and passwords, which makes it a useful target for scripting - without having to install an additional authentication plugin such as &lt;a href="https://datasette.io/plugins/datasette-auth-tokens"&gt;datasette-auth-tokens&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'm continuing to flesh out authentication options for Datasette, and adding this to &lt;code&gt;datasette-auth-passwords&lt;/code&gt; is one of those small improvements that should pay off long into the future.&lt;/p&gt;
&lt;h4&gt;A fix for datasette-publish-vercel&lt;/h4&gt;
&lt;p&gt;Datasette instances published to &lt;a href="https://vercel.com/"&gt;Vercel&lt;/a&gt; using the &lt;a href="https://datasette.io/plugins/datasette-publish-vercel"&gt;datasette-publish-vercel&lt;/a&gt; have previously been affected by an obscure Vercel bug: &lt;a href="https://github.com/vercel/vercel/issues/5575"&gt;characters such as + in the query string&lt;/a&gt; were being lost due to Vercel unescaping encoded characters before the request got to the Python application server.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://vercel.com/changelog/correcting-request-urls-with-python-serverless-functions"&gt;Vercel fixed this&lt;/a&gt; earlier this month, and the latest release of &lt;code&gt;datasette-publish-vercel&lt;/code&gt; includes their fix by switching to the new &lt;code&gt;@vercel/python&lt;/code&gt; builder. Thanks &lt;a href="https://twitter.com/styfle"&gt;@styfle&lt;/a&gt; from Vercel for shepherding this fix through!&lt;/p&gt;
&lt;h4&gt;New photos on Niche Museums&lt;/h4&gt;
&lt;p&gt;My Niche Museums project has been in hiberation since the start of the pandemic. Now that vaccines are rolling out it feels like there might be an end to this thing, so I've started thinking about my museum hobby again.&lt;/p&gt;
&lt;p&gt;I added some new photos to the site today - on the entries for &lt;a href="https://www.niche-museums.com/17"&gt;Novelty Automation&lt;/a&gt;, &lt;a href="https://www.niche-museums.com/21"&gt;DEVIL-ish Little Things&lt;/a&gt;, &lt;a href="https://www.niche-museums.com/24"&gt;Evergreen Aviation &amp;amp; Space Museum&lt;/a&gt; and &lt;a href="https://www.niche-museums.com/33"&gt;California State Capitol Dioramas&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Hopefully someday soon I'll get to visit and add an entirely new museum!&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/django-sql-dashboard"&gt;django-sql-dashboard&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/django-sql-dashboard/releases/tag/0.4a1"&gt;0.4a1&lt;/a&gt; - (&lt;a href="https://github.com/simonw/django-sql-dashboard/releases"&gt;10 releases total&lt;/a&gt;) - 2021-03-21
&lt;br /&gt;Django app for building dashboards using raw SQL queries&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.9.2"&gt;0.9.2&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-publish-vercel/releases"&gt;14 releases total&lt;/a&gt;) - 2021-03-20
&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-auth-passwords"&gt;datasette-auth-passwords&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-auth-passwords/releases/tag/0.4"&gt;0.4&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-auth-passwords/releases"&gt;9 releases total&lt;/a&gt;) - 2021-03-19
&lt;br /&gt;Datasette plugin for authentication using passwords&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/authentication"&gt;authentication&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dashboard"&gt;dashboard&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django-sql-dashboard"&gt;django-sql-dashboard&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sphinx-docs"&gt;sphinx-docs&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="authentication"/><category term="dashboard"/><category term="django"/><category term="postgresql"/><category term="projects"/><category term="zeit-now"/><category term="weeknotes"/><category term="django-sql-dashboard"/><category term="sphinx-docs"/></entry><entry><title>Weeknotes: Hacking on 23 different projects</title><link href="https://simonwillison.net/2020/Apr/16/weeknotes-hacking-23-different-projects/#atom-tag" rel="alternate"/><published>2020-04-16T05:03:11+00:00</published><updated>2020-04-16T05:03:11+00:00</updated><id>https://simonwillison.net/2020/Apr/16/weeknotes-hacking-23-different-projects/#atom-tag</id><summary type="html">
    &lt;p&gt;I wrote a lot of code this week: 184 commits over 23 repositories! I've also started falling for Zeit Now v2, having found workarounds for some of my biggest problems with it.&lt;/p&gt;

&lt;h3&gt;Better Datasette on Zeit Now v2&lt;/h3&gt;

&lt;p&gt;Last week I &lt;a href="https://simonwillison.net/2020/Apr/8/weeknotes-zeit-now-v2/"&gt;bemoaned the loss of Zeit Now v1&lt;/a&gt; and documented &lt;a href="https://simonwillison.net/2020/Apr/8/weeknotes-zeit-now-v2/#hello-zeit-now-v2"&gt;my initial explorations&lt;/a&gt; of Zeit Now v2 with respect to Datasette.&lt;/p&gt;

&lt;p&gt;My favourite thing about Now v1 was that it ran from Dockerfiles, which gave me complete control over the versions of everything in my deployment environment.&lt;/p&gt;

&lt;p&gt;Now v2 runs on AWS Lambda, which means you are mostly stuck with what Zeit's flavour of Lambda gives you. This currently means Python 3.6 (not too terrible - Datasette fully supports it) and a positively ancient SQLite -  3.7.17 from May 2013.&lt;/p&gt;

&lt;p&gt;Lambda runs on Amazon Linux. Charles Leifer maintains a package called &lt;a href="https://github.com/coleifer/pysqlite3/"&gt;pysqlite3&lt;/a&gt; which bundles the latest version of SQLite3 as a standalone Python package, and includes a &lt;code&gt;pysqlite3-binary&lt;/code&gt; package precompiled for Linux. Could it work on Amazon Linux...?&lt;/p&gt;

&lt;p&gt;It turns out it does! A &lt;a href="https://github.com/simonw/datasette-publish-now/commit/529f978beeccbb45240d398a3bf24ed9d77ebd55"&gt;one-line change&lt;/a&gt; (not including tests) to my &lt;a href="https://github.com/simonw/datasette-publish-now"&gt;datasette-publish-now&lt;/a&gt; and it now deploys Datasette on Now v2 &lt;a href="https://datasette-public.now.sh/-/versions"&gt;with SQLite 3.31.1&lt;/a&gt; - the &lt;a href="https://www.sqlite.org/changes.html#version_3_31_0"&gt;latest release&lt;/a&gt; from January this year, with window functions and all kinds of other goodness.&lt;/p&gt;

&lt;p&gt;This means that Now v2 is back to being a really solid option for hosting Datasette instances. You get scale-to-zero, crazily low prices and really fast cold-boot times. It can only take databases up to around 50MB - if you need more space than that you're better off with &lt;a href="https://datasette.readthedocs.io/en/stable/publish.html#publishing-to-google-cloud-run"&gt;Cloud Run&lt;/a&gt; - but it's a great option for smaller data.&lt;/p&gt;

&lt;p&gt;I released &lt;a href="https://github.com/simonw/datasette-publish-now/releases"&gt;a few versions of datasette-publish-now&lt;/a&gt; as a result of this research. I plan to release the first non-alpha version at the same time as Datasette 0.40.&lt;/p&gt;

&lt;h3&gt;Various projects ported to Now v2 or Cloud Run&lt;/h3&gt;

&lt;p&gt;I had over 100 projects running on Now v1 that needed updating or deleting in time for that platform's shutdown in August. I've been porting some of them very quickly using &lt;code&gt;datasette-publish-now&lt;/code&gt;, but a few have been more work. Some highlights from this week:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;&lt;a href="https://ftfy.now.sh/"&gt;ftfy.now.sh&lt;/a&gt;, my web app that takes a string of broken unicode and figures out the sequence of transformations you can use to make sense of it (built on the incredible &lt;a href="https://github.com/LuminosoInsight/python-ftfy"&gt;FTFY Python library&lt;/a&gt; by Robyn Speer) has been upgraded to Now v2 - &lt;a href="https://github.com/simonw/ftfy-web"&gt;repo here&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href="https://gzthermal.now.sh"&gt;gzthermal.now.sh&lt;/a&gt; offers a web interface to the &lt;code&gt;gzthermal&lt;/code&gt; gzip visualization tool, released by caveman &lt;a href="https://encode.su/threads/1889-gzthermal-pseudo-thermal-view-of-Gzip-Deflate-compression-efficiency"&gt;on the encode.ru (now encode.su) forum&lt;/a&gt;. My &lt;a href="https://github.com/simonw/gzthermal-web"&gt;repo is here&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;My &lt;a href="https://github.com/simonw/cryptozoology"&gt;crowdsourced directory of range maps of cryptozoological creatures&lt;/a&gt; is now running on Cloud Run (I haven't figured out a way to run SpatiaLite on Now v2 yet).&lt;/li&gt;&lt;li&gt;The &lt;a href="https://datasette-sqlite-fts4.datasette.io/24ways-fts4-52e8a02?sql=select%0D%0A++++title%2C+author%2C%0D%0A++++decode_matchinfo%28matchinfo%28articles_fts%2C+%22pcx%22%29%29%2C%0D%0A++++json_object%28%22pre%22%2C+annotate_matchinfo%28matchinfo%28articles_fts%2C+%22pcxnalyb%22%29%2C+%22pcxnalyb%22%29%29%0D%0Afrom%0D%0A++++articles_fts%0D%0Awhere%0D%0A++++articles_fts+match+%3Asearch&amp;amp;search=jquery+maps"&gt;datasette-sqlite-fts4.datasette.io&lt;/a&gt; demo instance I used for explanations in &lt;a href="https://simonwillison.net/2019/Jan/7/exploring-search-relevance-algorithms-sqlite/"&gt;Exploring search relevance algorithms with SQLite&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;The demo instance used for &lt;a href="https://github.com/simonw/datasette-jellyfish"&gt;datasette-jellyfish&lt;/a&gt; is on Now v2.&lt;/li&gt;&lt;li&gt;The demo for &lt;a href="https://github.com/simonw/datasette-jq"&gt;datasette-jq&lt;/a&gt; had to move to Cloud Run, because I couldn't install &lt;a href="https://github.com/doloopwhile/pyjq"&gt;pyjq&lt;/a&gt; on Now v2.&lt;/li&gt;&lt;/ul&gt;

&lt;h3&gt;big-local-datasette&lt;/h3&gt;

&lt;p&gt;I've been collaborating with the &lt;a href="https://biglocalnews.org/"&gt;Big Local&lt;/a&gt; team at Stanford on a number of projects related to the Covid-19 situation. It's not quite open to the public yet but I've been building a Datasette instance which shares data from the "open projects" maintained by that team.&lt;/p&gt;

&lt;p&gt;The implementation fits &lt;a href="https://simonwillison.net/2020/Jan/21/github-actions-cloud-run/"&gt;a common pattern&lt;/a&gt; for me: a &lt;a href="https://github.com/simonw/big-local-datasette/blob/afcb885b3e746d6380f4ad6bab899190b461975d/.github/workflows/deploy.yml"&gt;scheduled GitHub Action&lt;/a&gt; which fetches project data from a GraphQL API, seeks out CSV files which have changed (using HTTP HEAD requests to check their ETags), loads the CSV into SQLite tables and publishes the resulting database using &lt;code&gt;datasette publish cloudrun&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There's one interesing new twist: I'm fetching the existing database files on every run using my new &lt;a href="https://simonwillison.net/2020/Apr/14/datasette-clone/"&gt;datasette-clone tool&lt;/a&gt; (written for this project), applying changes to them and then only publishing if the resulting MD5 sums have changed since last time.&lt;/p&gt;

&lt;p&gt;It seems to work well, and I'm excited about this technique as a way of incrementally updating existing databases using stateless code running in a GitHub Action.&lt;/p&gt;

&lt;h3&gt;Datasette Cloud&lt;/h3&gt;

&lt;p&gt;I continue to work on the invite-only alpha of my SaaS Datasette platform, Datasette Cloud. This week I ported the CI and deployment scripts from GitLab to GitHub Actions, mainly to try and reduce the variety of CI systems I'm working with (I now have projects live on three: Travis, Circle CI and GitHub Actions).&lt;/p&gt;

&lt;p&gt;I've also been figuring out ways of supporting API tokens for making requests to authentication-protected Datasette instances. I shipped small releases of &lt;a href="https://github.com/simonw/datasette-auth-github/releases/tag/0.12"&gt;datasette-auth-github&lt;/a&gt; and &lt;a href="https://github.com/simonw/datasette-auth-existing-cookies/releases/tag/0.7"&gt;datasette-auth-existing-cookies&lt;/a&gt; to support this.&lt;/p&gt;

&lt;p&gt;In tinkering with Datasette Cloud I also shipped an upgrade to &lt;a href="https://github.com/simonw/datasette-mask-columns"&gt;datasette-mask-columns&lt;/a&gt;, which now shows visible REDACTED text on redacted columns in table view.&lt;/p&gt;

&lt;h3&gt;Miscellaneous&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;My &lt;a href="https://covid-19.datasettes.com/"&gt;covid-19.datasettes.com&lt;/a&gt; project now also imports data &lt;a href="https://github.com/simonw/covid-19-datasette/issues/11"&gt;from the LA Times&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;I added &lt;code&gt;.rows_where(..., order_by="column")&lt;/code&gt; in &lt;a href="https://sqlite-utils.readthedocs.io/en/stable/changelog.html#v2-6"&gt;release 2.6 of sqlite-utils&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;I shipped a &lt;a href="https://github.com/simonw/paginate-json/releases/tag/0.3"&gt;new release&lt;/a&gt; of &lt;a href="https://github.com/simonw/paginate-json"&gt;paginate-json&lt;/a&gt;, a tool I built primarily for paginating through the GitHub API and piping the results to &lt;code&gt;sqlite-utils&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;I fixed a minor bug &lt;a href="https://github.com/simonw/datasette/issues/724"&gt;with Datasette's --plugin-secret mechanism&lt;/a&gt; and added &lt;a href="https://github.com/simonw/datasette/issues/727"&gt;a CSS customization hook&lt;/a&gt; for the canned query page.&lt;/li&gt;&lt;li&gt;I built a &lt;a href="https://github.com/simonw/heic-to-jpeg"&gt;HEIC to JPEG converting proxy&lt;/a&gt; as part of my ongoing mission to eventually liberate my photos from Apple Photos and make them available to &lt;a href="https://simonwillison.net/tags/dogsheep/"&gt;Dogsheep&lt;/a&gt;. In doing so I &lt;a href="https://github.com/david-poirier-csn/pyheif/commit/8d03e0bf6dde6aa0317471792d698a30502f9e1d?short_path=04c6e90#diff-04c6e90faac2675aa89e2176d2eec7d8"&gt;contributed usage documentation&lt;/a&gt; to the pyheif Python library.&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/zeit-now"&gt;zeit-now&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dogsheep"&gt;dogsheep&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;/p&gt;
    

</summary><category term="projects"/><category term="zeit-now"/><category term="datasette"/><category term="dogsheep"/><category term="weeknotes"/><category term="datasette-cloud"/></entry><entry><title>Goodbye Zeit Now v1, hello datasette-publish-now - and talking to myself in GitHub issues</title><link href="https://simonwillison.net/2020/Apr/8/weeknotes-zeit-now-v2/#atom-tag" rel="alternate"/><published>2020-04-08T03:32:24+00:00</published><updated>2020-04-08T03:32:24+00:00</updated><id>https://simonwillison.net/2020/Apr/8/weeknotes-zeit-now-v2/#atom-tag</id><summary type="html">
    &lt;p&gt;This week I’ve been mostly dealing with the finally announced shutdown of Zeit Now v1. And having long-winded conversations with myself in GitHub issues.&lt;/p&gt;

&lt;h3&gt;How Zeit Now inspired Datasette&lt;/h3&gt;

&lt;p&gt;I first started experiencing with Zeit’s serverless &lt;a href="https://zeit.co/home"&gt;Now&lt;/a&gt; hosting platform back &lt;a href="https://simonwillison.net/2017/Oct/14/async-python-sanic-now/"&gt;in October 2017&lt;/a&gt;, when I used it to deploy &lt;a href="https://json-head.now.sh/"&gt;json-head.now.sh&lt;/a&gt; - an updated version of an API tool I originally built for Google App Engine &lt;a href="https://simonwillison.net/2008/Jul/29/jsonhead/"&gt;in July 2008&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I liked Zeit Now, a lot. Instant, inexpensive deploys of any stateless project that could be defined using a Dockerfile? Just type &lt;code&gt;now&lt;/code&gt; to deploy the project in your current directory? Every deployment gets its own permanent URL? Amazing!&lt;/p&gt;

&lt;p&gt;There was just one catch: Since Now deployments are ephemeral applications running on them need to be stateless. If you want a database, you need to involve another (potentially costly) service. It's a limitation shared by other scalable hosting solutions - Heroku, App Engine and so on. How much interesting stuff can you build without a database?&lt;/p&gt;

&lt;p&gt;I was musing about this in the shower one day (that &lt;a href="https://lifehacker.com/science-explains-why-our-best-ideas-come-in-the-shower-5987858"&gt;old cliche&lt;/a&gt; really happened for me) when I had a thought: sure, you can't write to a database... but if your data is read-only, why not bundle the database alongside the application code as part of the Docker image?&lt;/p&gt;

&lt;p&gt;Ever since I &lt;a href="https://simonwillison.net/2009/Mar/10/openplatform/"&gt;helped launch the Datablog&lt;/a&gt; at the Guardian back in 2009 I had been interested in finding better ways to publish data journalism datasets than CSV files or a Google spreadsheets - so building something that could package and bundle read-only data was of extreme interest to me.&lt;/p&gt;

&lt;p&gt;In November 2017 I released &lt;a href="https://simonwillison.net/2017/Nov/13/datasette/"&gt;the first version&lt;/a&gt; of Datasette. The original idea was very much inspired by Zeit Now.&lt;/p&gt;

&lt;p&gt;I gave &lt;a href="https://www.youtube.com/watch?v=_uwrqB--eM4"&gt;a talk about Datasette&lt;/a&gt; at the Zeit Day conference in San Francisco in April 2018. Suffice to say I was a huge fan!&lt;/p&gt;

&lt;h3&gt;Goodbye, Zeit Now v1&lt;/h3&gt;

&lt;p&gt;In November 2018, Zeit &lt;a href="https://simonwillison.net/2018/Nov/19/smaller-python-docker-images/"&gt;announced Now v2&lt;/a&gt;. And it was, &lt;em&gt;different&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;v2 is an entirely different architecture from v1. Where v1 built on Docker containers, v2 is built on top of serverless functions - AWS Lambda in particular.&lt;/p&gt;

&lt;p&gt;I can see why Zeit did this. Lambda functions can launch from cold &lt;em&gt;way faster&lt;/em&gt; - v1's Docker infrastructure had tough cold-start times. They are much cheaper to run as well - crucial for Zeit given their &lt;a href="https://zeit.co/pricing"&gt;extremely generous pricing plans&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But it was bad news for my projects. Lambdas are tightly size constrained, which is tough when you're bundling potentially large SQLite database files with your deployments.&lt;/p&gt;

&lt;p&gt;More importantly, in 2018 Amazon were deliberately excluding the Python &lt;code&gt;sqlite3&lt;/code&gt; standard library module from the Python Lambda environment! I guess they hadn't considered people who might want to work with read-only database files.&lt;/p&gt;

&lt;p&gt;So Datasette on Now v2 just wasn't going to work. Zeit kept v1 supported for the time being, but the writing was clearly on the wall.&lt;/p&gt;

&lt;p&gt;In April 2019 &lt;a href="https://cloud.google.com/blog/products/serverless/announcing-cloud-run-the-newest-member-of-our-serverless-compute-stack"&gt;Google announced Cloud Run&lt;/a&gt;, a serverless, scale-to-zero hosting environment based around Docker containers. In many ways it's Google's version of Zeit Now v1 - it has many of the characteristics I loved about v1, albeit with a clunkier developer experience and much more friction in assigning nice URLs to projects. Romain Primet &lt;a href="https://github.com/simonw/datasette/pull/434"&gt;contributed Cloud Run support to Datasette&lt;/a&gt; and it has since become my preferred hosting target for my new projects (see &lt;a href="https://simonwillison.net/2020/Jan/21/github-actions-cloud-run/"&gt;Deploying a data API using GitHub Actions and Cloud Run&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Last week, Zeit &lt;a href="https://twitter.com/simonw/status/1246300304917680128"&gt;finally announced&lt;/a&gt; the sunset date for v1. From 1st of May new deploys won't be allowed, and on the 7th of August they'll be turning off the old v1 infrastructure and deleting all existing Now v1 deployments.&lt;/p&gt;

&lt;p&gt;I engaged in &lt;a href="https://twitter.com/simonw/status/1246300304917680128"&gt;an extensive Twitter conversation&lt;/a&gt; about this, where I praised Zeit's handling of the shutdown while bemoaning the loss of the v1 product I had loved so much.&lt;/p&gt;

&lt;h3 id="migrating-my-projects"&gt;Migrating my projects&lt;/h3&gt;

&lt;p&gt;My newer projects have been on Cloud Run for quite some time, but I still have a bunch of old projects that I care about and want to keep running past the v1 shutdown.&lt;/p&gt;

&lt;p&gt;The first project I ported was &lt;a href="https://latest.datasette.io/"&gt;latest.datasette.io&lt;/a&gt;, a live demo of Datasette which updates with the latest code any time I push to the Datasette master branch on GitHub.&lt;/p&gt;

&lt;p&gt;Any time I do some kind of ops task like this I've gotten into the habit of meticulously documenting every single step in comments on a GitHub issue. Here's &lt;a href="https://github.com/simonw/datasette/issues/705"&gt;the issue&lt;/a&gt; for porting latest.datasette.io to Cloud Run (and switching from Circle CI to GitHub Actions at the same time).&lt;/p&gt;

&lt;p&gt;My next project was &lt;a href="https://global-power-plants.datasettes.com/global-power-plants/global-power-plants"&gt;global-power-plants-datasette&lt;/a&gt;, a small project which takes a database of global power plants &lt;a href="https://www.wri.org/publication/global-power-plant-database"&gt;published by the World Resources Institute&lt;/a&gt; and publishes it using Datasette. It checks for new updates to &lt;a href="https://github.com/wri/global-power-plant-database"&gt;their repo&lt;/a&gt; once a day. I originally built it as a demo for &lt;a href="https://github.com/simonw/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt;, since it's fun seeing 33,000 power plants on a single map. Here's &lt;a href="https://github.com/simonw/global-power-plants-datasette/issues/1"&gt;that issue&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Having warmed up with these two, my next target was the most significant: porting my &lt;a href="https://www.niche-museums.com/"&gt;Niche Museums&lt;/a&gt; website.&lt;/p&gt;

&lt;p&gt;Niche Museums is the most heavily customized Datasette instance I've run anywhere - it incorporates custom templates, CSS and plugins.&lt;/p&gt;

&lt;p&gt;Here's &lt;a href="https://github.com/simonw/museums/issues/20"&gt;the tracking issue&lt;/a&gt; for porting it to Cloud Run. I ran into a few hurdles with DNS and TLS certificates, and I had to do &lt;a href="https://github.com/simonw/museums/issues/21"&gt;some additional work&lt;/a&gt; to ensure &lt;code&gt;niche-museums.com&lt;/code&gt; redirects to &lt;code&gt;www.niche-musums.com&lt;/code&gt;, but it's now fully migrated.&lt;/p&gt;

&lt;h3 id="hello-zeit-now-v2"&gt;Hello, Zeit Now v2&lt;/h3&gt;

&lt;p&gt;In &lt;a href="https://twitter.com/simonw/status/1246302021608591360"&gt;complaining about&lt;/a&gt; the lack of that essential &lt;code&gt;sqlite3&lt;/code&gt; module I figured it would be responsible to double-check and make sure that was still true.&lt;/p&gt;

&lt;p&gt;It was not! Today Now's Python environment &lt;a href="https://twitter.com/simonw/status/1246600935289184256"&gt;includes sqlite3&lt;/a&gt; after all.&lt;/p&gt;

&lt;p&gt;Datasette's &lt;a href="https://datasette.readthedocs.io/en/0.39/plugins.html#publish-subcommand-publish"&gt;publish_subcommand() plugin hook&lt;/a&gt; lets plugins add new publishing targets to the &lt;code&gt;datasette publish&lt;/code&gt; command (I used it to build &lt;a href="https://github.com/simonw/datasette-publish-fly"&gt;datasette-publish-fly&lt;/a&gt; last month). How hard would it be to build a plugin for Zeit Now v2?&lt;/p&gt;

&lt;p&gt;I fired up a new &lt;a href="https://github.com/simonw/datasette/issues/717"&gt;lengthy talking-to-myself GitHub issue&lt;/a&gt; and started prototyping.&lt;/p&gt;

&lt;p&gt;Now v2 may not support Docker, but it does support the &lt;a href="https://asgi.readthedocs.io/en/latest/"&gt;ASGI Python standard&lt;/a&gt; (the asynchronous alternative to WSGI, shepherded by Andrew Godwin).&lt;/p&gt;

&lt;p&gt;Zeit are keen proponents of the &lt;a href="https://jamstack.org/"&gt;Jamstack&lt;/a&gt; approach, where websites are built using static pre-rendered HTML and JavaScript that calls out to APIs for dynamic data. v2 deployments are expected to consist of static HTML with "serverless functions" - standalone server-side scripts that live in an &lt;code&gt;api/&lt;/code&gt; directory by convention and are compiled into separate lambdas.&lt;/p&gt;

&lt;p&gt;Datasette works just fine without JavaScript, which means it needs to handle all of the URL routes for a site. Essentually I need to build a single function that runs the whole of Datasette, then route all incoming traffic to it.&lt;/p&gt;

&lt;p&gt;It took me a while to figure it out, but it turns out the Now v2 recipe for that is a &lt;code&gt;now.json&lt;/code&gt; file that looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
    "version": 2,
    "builds": [
        {
            "src": "index.py",
            "use": "@now/python"
        }
    ],
    "routes": [
        {
            "src": "(.*)",
            "dest": "index.py"
        }
    ]
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Thanks Aaron Boodman for &lt;a href="https://twitter.com/aboodman/status/1246605658067066882"&gt;the tip&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Given the above configuration, Zeit will install any Python dependencies in a &lt;code&gt;requirements.txt&lt;/code&gt; file, then treat an &lt;code&gt;app&lt;/code&gt; variable in the &lt;code&gt;index.py&lt;/code&gt; file as an ASGI application it should route all incoming traffic to. Exactly what I need to deploy Datasette!&lt;/p&gt;

&lt;p&gt;This was everything I needed to build the new plugin. &lt;a href="https://github.com/simonw/datasette-publish-now"&gt;datasette-publish-now&lt;/a&gt; is the result.&lt;/p&gt;

&lt;p&gt;Here's &lt;a href="https://datasette-public.now.sh/_src"&gt;the generated source code&lt;/a&gt; for a project deployed using the plugin, showing how the underlyinng ASGI application is configured.&lt;/p&gt;

&lt;p&gt;It's currently an alpha - not every feature is supported (see &lt;a href="https://github.com/simonw/datasette-publish-now/milestone/1"&gt;this milestone&lt;/a&gt;) and it relies on a minor deprecated feature (which I've &lt;a href="https://github.com/zeit/now/discussions/4021"&gt;implored Zeit to reconsider&lt;/a&gt;) but it's already full-featured enough that I can start using it to upgrade some of my smaller existing Now projects.&lt;/p&gt;

&lt;p&gt;The first I upgraded is one of my favourites: &lt;a href="https://polar-bears.now.sh/"&gt;polar-bears.now.sh&lt;/a&gt;, which visualizes tracking data from polar bear ear tags (using &lt;a href="https://github.com/simonw/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt;) that was &lt;a href="https://alaska.usgs.gov/products/data.php?dataid=130"&gt;published by the USGS Alaska Science Center, Polar Bear Research Program&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here's the command I used to deploy the site:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ pip install datasette-publish-now
$ datasette publish now2 polar-bears.db \
    --title "Polar Bear Ear Tags, 2009-2011" \
    --source "USGS Alaska Science Center, Polar Bear Research Program" \
    --source_url "https://alaska.usgs.gov/products/data.php?dataid=130" \
    --install datasette-cluster-map \
    --project=polar-bears&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I exported a full list of my Now v1 projects from their handy &lt;a href="https://zeit.co/dashboard/active-v1-instances"&gt;active v1 instances&lt;/a&gt; page.&lt;/p&gt;

&lt;h3&gt;The rest of my projects&lt;/h3&gt;

&lt;p&gt;I scraped the page using the following JavaScript, constructed with the help of the &lt;a href="https://simonwillison.net/2020/Apr/7/new-developer-features-firefox-75/"&gt;instant evaluation&lt;/a&gt; console feature in Firefox 75:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;console.log(
  JSON.stringify(
    Array.from(
      Array.from(
        document.getElementsByTagName("table")[1].
          getElementsByTagName("tr")
      ).slice(1).map(
        (tr) =&amp;gt;
          Array.from(
            tr.getElementsByTagName("td")
        ).map((td) =&amp;gt; td.innerText)
      )
    )
  )
);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then I loaded them into Datasette for analysis.&lt;/p&gt;

&lt;p&gt;After filtering out the &lt;code&gt;datasette-latest-commithash.now.sh&lt;/code&gt; projects I had deployed for every push to GitHub it turns out I have 34 distinct projects running there.&lt;/p&gt;

&lt;p&gt;I won't port all of them, but given &lt;code&gt;datasette-publish-now&lt;/code&gt; I should be able to port the ones that I care about without too much trouble.&lt;/p&gt;

&lt;h3 id="git-bisect"&gt;Debugging Datasette with git bisect run&lt;/h3&gt;

&lt;p&gt;I fixed two bugs in Datasette this week using &lt;code&gt;git bisect run&lt;/code&gt; - a tool I've been meaning to figure out for years, which lets you run an automated binary search against a commit log to find the source of a bug.&lt;/p&gt;

&lt;p&gt;Since I was figuring out a new tool, I fired up another GitHub issue self-conversation: in &lt;a href="https://github.com/simonw/datasette/issues/716"&gt;issue #716&lt;/a&gt; I document my process of both learning to use &lt;code&gt;git bisect run&lt;/code&gt; and using it to find a solution to that particular bug.&lt;/p&gt;

&lt;p&gt;It worked great, so I used the same trick on &lt;a href="https://github.com/simonw/datasette/issues/689"&gt;issue 689&lt;/a&gt; as well.&lt;/p&gt;

&lt;p&gt;Watching &lt;code&gt;git bisect run&lt;/code&gt; churn through 32 revisions in a few seconds and pinpoint the exact moment a bug was introduced is pretty delightful:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;$ git bisect start master 0.34
Bisecting: 32 revisions left to test after this (roughly 5 steps)
[dc80e779a2e708b2685fc641df99e6aae9ad6f97] Handle scope path if it is a string
$ git bisect run python check_templates_considered.py
running python check_templates_considered.py
Traceback (most recent call last):
...
AssertionError
Bisecting: 15 revisions left to test after this (roughly 4 steps)
[7c6a9c35299f251f9abfb03fd8e85143e4361709] Better tests for prepare_connection() plugin hook, refs #678
running python check_templates_considered.py
Traceback (most recent call last):
...
AssertionError
Bisecting: 7 revisions left to test after this (roughly 3 steps)
[0091dfe3e5a3db94af8881038d3f1b8312bb857d] More reliable tie-break ordering for facet results
running python check_templates_considered.py
Traceback (most recent call last):
...
AssertionError
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[ce12244037b60ba0202c814871218c1dab38d729] Release notes for 0.35
running python check_templates_considered.py
Traceback (most recent call last):
...
AssertionError
Bisecting: 1 revision left to test after this (roughly 1 step)
[70b915fb4bc214f9d064179f87671f8a378aa127] Datasette.render_template() method, closes #577
running python check_templates_considered.py
Traceback (most recent call last):
...
AssertionError
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[286ed286b68793532c2a38436a08343b45cfbc91] geojson-to-sqlite
running python check_templates_considered.py
70b915fb4bc214f9d064179f87671f8a378aa127 is the first bad commit
commit 70b915fb4bc214f9d064179f87671f8a378aa127
Author: Simon Willison
Date:   Tue Feb 4 12:26:17 2020 -0800

    Datasette.render_template() method, closes #577

    Pull request #664.

:040000 040000 def9e31252e056845609de36c66d4320dd0c47f8 da19b7f8c26d50a4c05e5a7f05220b968429725c M	datasette
bisect run success&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Supporting metadata.yaml&lt;/h3&gt;

&lt;p&gt;The other Datasette project I completed this week is a relatively small feature with hopefully a big impact: you can &lt;a href="https://github.com/simonw/datasette/issues/713"&gt;now use YAML for Datasette's metadata configuration&lt;/a&gt; as an alternative to JSON.&lt;/p&gt;

&lt;p&gt;I'm not crazy about YAML: I still don't feel like I've mastered it, and I've been &lt;a href="https://simonwillison.net/tags/yaml/"&gt;tracking it for 18 years&lt;/a&gt;! But it has one big advantage over JSON for configuration files: robust support for multi-line strings.&lt;/p&gt;

&lt;p&gt;Datasette's &lt;a href="https://datasette.readthedocs.io/en/latest/metadata.html"&gt;metadata file&lt;/a&gt; can include lengthy SQL statements and strings of HTML, both of which benefit from multi-line strings.&lt;/p&gt;

&lt;p&gt;I first used YAML for metadata for my &lt;a href="https://simonwillison.net/2018/Aug/6/russian-facebook-ads/"&gt;Analyzing US Election Russian Facebook Ads&lt;/a&gt; project. The &lt;a href="https://github.com/simonw/russian-ira-facebook-ads-datasette/blob/336ba87ef8071e664441ad0a95e3b8d0a33f682a/russian-ads-metadata.yaml"&gt;metadata file for that&lt;/a&gt; demonstrates both embedded HTML and embedded SQL - and an accompanying &lt;a href="https://github.com/simonw/russian-ira-facebook-ads-datasette/blob/336ba87ef8071e664441ad0a95e3b8d0a33f682a/build_metadata.py"&gt;build_metadata.py&lt;/a&gt; script converted it to JSON at build time. I've since used the same trick for a number of other projects.&lt;/p&gt;

&lt;p&gt;The next release of Datasette (hopefully within a week) will ship the new feature, at which point those conversion scripts won't be necessary.&lt;/p&gt;

&lt;p&gt;This should work particularly well with the forthcoming &lt;a href="https://github.com/simonw/datasette/issues/698"&gt;ability for a canned query to write to a database&lt;/a&gt;. Getting that wrapped up and shipped will be my focus for the next few days.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/git"&gt;git&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/yaml"&gt;yaml&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&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-issues"&gt;github-issues&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="git"/><category term="github"/><category term="projects"/><category term="yaml"/><category term="zeit-now"/><category term="datasette"/><category term="weeknotes"/><category term="github-issues"/></entry><entry><title>Zeit Now v1 to sunset soon: no new deployments from 1st May, total shutdown 7th August</title><link href="https://simonwillison.net/2020/Apr/4/zeit-now-v1-sunset-soon-no-new-deployments-1st-may-total-shutdow/#atom-tag" rel="alternate"/><published>2020-04-04T05:32:02+00:00</published><updated>2020-04-04T05:32:02+00:00</updated><id>https://simonwillison.net/2020/Apr/4/zeit-now-v1-sunset-soon-no-new-deployments-1st-may-total-shutdow/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://twitter.com/simonw/status/1246300304917680128"&gt;Zeit Now v1 to sunset soon: no new deployments from 1st May, total shutdown 7th August&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I posted a thread on Twitter with some thoughts. Zeit Now v1 remains the best hosting platform I’ve ever used given my particular tastes. They’ve handled the shutdown very responsibly, but I’m sad to see it go.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hosting"&gt;hosting&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;



</summary><category term="hosting"/><category term="zeit-now"/><category term="datasette"/></entry><entry><title>Ministry of Silly Runtimes: Vintage Python on Cloud Run</title><link href="https://simonwillison.net/2019/Apr/9/vintage-python-on-cloud-run/#atom-tag" rel="alternate"/><published>2019-04-09T17:33:47+00:00</published><updated>2019-04-09T17:33:47+00:00</updated><id>https://simonwillison.net/2019/Apr/9/vintage-python-on-cloud-run/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/di/ministry-of-silly-runtimes-vintage-python-on-cloud-run-3b9d"&gt;Ministry of Silly Runtimes: Vintage Python on Cloud Run&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Cloud Run is an exciting new hosting service from Google that lets you define a container using a Dockerfile and then run that container in a “scale to zero” environment, so you only pay for time spent serving traffic. It’s similar to the now-deprecated Zeit Now 1.0 which inspired me to create Datasette. Here Dustin Ingram demonstrates how powerful Docker can be as the underlying abstraction by deploying a web app using a 25 year old version of Python 1.x.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cloud"&gt;cloud&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/docker"&gt;docker&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudrun"&gt;cloudrun&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dustin-ingram"&gt;dustin-ingram&lt;/a&gt;&lt;/p&gt;



</summary><category term="cloud"/><category term="python"/><category term="zeit-now"/><category term="docker"/><category term="datasette"/><category term="cloudrun"/><category term="dustin-ingram"/></entry><entry><title>Building smaller Python Docker images</title><link href="https://simonwillison.net/2018/Nov/19/smaller-python-docker-images/#atom-tag" rel="alternate"/><published>2018-11-19T03:13:40+00:00</published><updated>2018-11-19T03:13:40+00:00</updated><id>https://simonwillison.net/2018/Nov/19/smaller-python-docker-images/#atom-tag</id><summary type="html">
    &lt;p&gt;Changes are afoot at &lt;a href="https://zeit.co/now"&gt;Zeit Now&lt;/a&gt;, my preferred hosting provider for the past year (see &lt;a href="https://simonwillison.net/tags/zeitnow/"&gt;previous posts&lt;/a&gt;). They have &lt;a href="https://zeit.co/blog/now-2"&gt;announced Now 2.0&lt;/a&gt;, an intriguing new approach to providing auto-scaling immutable deployments. It’s built on top of lambdas, and comes with a whole host of new constraints: code needs to fit into a 5MB bundle for example (though it looks like this restriction will soon be &lt;a href="https://spectrum.chat/?t=0ab38384-5aa3-4b04-899a-5b056f9b83b9"&gt;relaxed a little&lt;/a&gt; - &lt;strong&gt;update November 19th&lt;/strong&gt; you can now &lt;a href="https://zeit.co/blog/customizable-lambda-sizes"&gt;bump this up to 50MB&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Unfortunately, they have also announced their intent to deprecate the existing Now v1 Docker-based solution.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“We will only start thinking about deprecation plans once we are able to accommodate the most common and critical use cases of v1 on v2” - &lt;a href="https://spectrum.chat/thread/96985341-e17f-4af4-a330-c726774ed436?m=MTU0MTcwOTU1ODIwNA=="&gt;Matheus Fernandes&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;“When we reach feature parity, we still intend to give customers plenty of time to upgrade (we are thinking at the very least 6 months from the time we announce it)” - &lt;a href="https://spectrum.chat/thread/46d54a53-f58d-4e8f-bce2-047a6ac93305?m=MTU0MjUyMDMwMzc5NQ=="&gt;Guillermo Rauch&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is pretty disastrous news for many of my projects, most crucially &lt;a href="https://github.com/simonw/datasette"&gt;Datasette&lt;/a&gt; and &lt;a href="https://simonwillison.net/2018/Jan/17/datasette-publish/"&gt;Datasette Publish&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Datasette should be fine - it supports Heroku as an alternative to Zeit Now &lt;a href="https://datasette.readthedocs.io/en/stable/publish.html"&gt;out of the box&lt;/a&gt;, and the &lt;a href="https://datasette.readthedocs.io/en/stable/plugins.html#publish-subcommand-publish"&gt;publish_subcommand plugin hook&lt;/a&gt; makes it easy to add further providers (I’m exploring several new options at the moment).&lt;/p&gt;
&lt;p&gt;Datasette Publish is a bigger problem. The whole point of that project is to make it easy for less-technical users to deploy their data as an interactive API to a Zeit Now account that they own themselves. Talking these users through what they need to do to upgrade should v1 be shut down in the future is not an exciting prospect.&lt;/p&gt;
&lt;p&gt;So I’m going to start hunting for an alternative backend for Datasette Publish, but in the meantime I’ve had to make some changes to how it works in order to handle a new size limit of 100MB for Docker images deployed by free users.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Building_smaller_Docker_images_18"&gt;&lt;/a&gt;Building smaller Docker images&lt;/h3&gt;
&lt;p&gt;Zeit &lt;a href="https://twitter.com/ppival/status/1063464380057055232"&gt;appear to have introduced a new limit&lt;/a&gt; for free users of their Now v1 platform: Docker images need to be no larger than 100MB.&lt;/p&gt;
&lt;p&gt;Datasette Publish was creating final image sizes of around 350MB, blowing way past that limit. I spent some time today figuring out how to get it to produce images within the new limit, and learned a lot about Docker image optimization in the process.&lt;/p&gt;
&lt;p&gt;I ended up using Docker’s &lt;a href="https://docs.docker.com/develop/develop-images/multistage-build/"&gt;multi-stage build feature&lt;/a&gt;, which allows you to create temporary images during a build, use them to  compile dependencies, then copy just the compiled assets into the final image.&lt;/p&gt;
&lt;p&gt;An example of the previous Datasette Publish generated Dockerfile &lt;a href="https://gist.github.com/simonw/365294fb51765fb07bc99fe5eb7fee22"&gt;can be seen here&lt;/a&gt;. Here’s a rough outline of what it does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start with the &lt;code&gt;python:3.6-slim-stretch&lt;/code&gt; image&lt;/li&gt;
&lt;li&gt;apt-installs &lt;code&gt;python3-dev&lt;/code&gt; and &lt;code&gt;gcc&lt;/code&gt; so it can compile Python libraries with binary dependencies (pandas and uvloop for example)&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;pip&lt;/code&gt; to install &lt;code&gt;csvs-to-sqlite&lt;/code&gt; and &lt;code&gt;datasette&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Add the uploaded CSV files, then run &lt;code&gt;csvs-to-sqlite&lt;/code&gt; to convert them into a SQLite database&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;datasette inspect&lt;/code&gt; to cache a JSON file with information about the different tables&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;datasette serve&lt;/code&gt; to serve the resulting web application&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There’s a lot of scope for improvement here. The final image has all sorts of cruft that’s not actually needed for serving the image: it has &lt;code&gt;csvs-to-sqlite&lt;/code&gt; and all of its dependencies, plus the original uploaded CSV files.&lt;/p&gt;
&lt;p&gt;Here’s the workflow I used to build a Dockerfile and check the size of the resulting image. My work-in-progress can be found in the &lt;a href="https://github.com/simonw/datasette-small"&gt;datasette-small repo&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Build the Dockerfile in the current directory and tag as datasette-small
$ docker build . -t datasette-small
# Inspect the size of the resulting image
$ docker images | grep datasette-small
# Start the container running
$ docker run -d -p 8006:8006 datasette-small
654d3fc4d3343c6b73414c6fb4b2933afc56fbba1f282dde9f515ac6cdbc5339
# Now visit http://localhost:8006/ to see it running
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a id="Alpine_Linux_48"&gt;&lt;/a&gt;Alpine Linux&lt;/h3&gt;
&lt;p&gt;When you start looking for ways to build smaller Dockerfiles, the first thing you will encounter is &lt;a href="https://en.wikipedia.org/wiki/Alpine_Linux"&gt;Alpine Linux&lt;/a&gt;. Alpine is a Linux distribution that’s perfect for containers: it builds on top of &lt;a href="https://en.wikipedia.org/wiki/BusyBoxAlpine_Linux"&gt;BusyBox&lt;/a&gt; to strip down to the smallest possible image that can still do useful things.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;python:3.6-alpine&lt;/code&gt; container should be perfect: it gives you the smallest possible container that can run Python 3.6 applications (including the ability to &lt;code&gt;pip install&lt;/code&gt; additional dependencies).&lt;/p&gt;
&lt;p&gt;There’s just one problem: in order to install C-based dependencies like &lt;a href="https://pandas.pydata.org/"&gt;pandas&lt;/a&gt; (used by csvs-to-sqlite) and &lt;a href="https://github.com/huge-success/sanic"&gt;Sanic&lt;/a&gt; (used by Datasette) you need a compiler toolchain. Alpine doesn’t have this out-of-the-box, but you can install one using Alpine’s &lt;code&gt;apk&lt;/code&gt; package manager. Of course, now you’re bloating your container with a bunch of compilation tools that you don’t need to serve the final image.&lt;/p&gt;
&lt;p&gt;This is what makes multi-stage builds so useful! We can spin up an Alpine image with the compilers installed, build our modules, then copy the resulting binary blobs into a fresh container.&lt;/p&gt;
&lt;p&gt;Here’s the basic recipe for doing that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM python:3.6-alpine as builder

# Install and compile Datasette + its dependencies
RUN apk add --no-cache gcc python3-dev musl-dev alpine-sdk
RUN pip install datasette

# Now build a fresh container, copying across the compiled pieces
FROM python:3.6-alpine

COPY --from=builder /usr/local/lib/python3.6 /usr/local/lib/python3.6
COPY --from=builder /usr/local/bin/datasette /usr/local/bin/datasette
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern works really well, and produces delightfully slim images. My first attempt at this wasn’t quite slim enough to fit the 100MB limit though, so I had to break out some Docker tools to figure out exactly what was going on.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Inspecting_docker_image_layers_74"&gt;&lt;/a&gt;Inspecting docker image layers&lt;/h3&gt;
&lt;p&gt;Part of the magic of Docker is the concept of &lt;a href="https://medium.com/@jessgreb01/digging-into-docker-layers-c22f948ed612"&gt;layers&lt;/a&gt;. When Docker builds a container it uses a layered filesystem (&lt;a href="https://en.wikipedia.org/wiki/UnionFS"&gt;UnionFS&lt;/a&gt;) and creates a new layer for every executable line in the Dockerfile. This dramatically speeds up future builds (since layers can be reused if they have already been built) and also provides a powerful tool for inspecting different stages of the build.&lt;/p&gt;
&lt;p&gt;When you run &lt;code&gt;docker build&lt;/code&gt; part of the output is IDs of the different image layers as they are constructed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette-small $ docker build . -t datasette-small
Sending build context to Docker daemon  2.023MB
Step 1/21 : FROM python:3.6-slim-stretch as csvbuilder
 ---&amp;gt; 971a5d5dad01
Step 2/21 : RUN apt-get update &amp;amp;&amp;amp; apt-get install -y python3-dev gcc wget
 ---&amp;gt; Running in f81485df62dd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Given a layer ID, like &lt;code&gt;971a5d5dad01&lt;/code&gt;, it’s possible to spin up a new container that exposes the exact state of that layer (&lt;a href="https://stackoverflow.com/a/26222636/6083"&gt;thanks, Stack Overflow&lt;/a&gt;). Here’s how do to that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -it --rm 971a5d5dad01 sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;-it&lt;/code&gt; argument attaches standard input to the container (&lt;code&gt;-i&lt;/code&gt;) and allocates a pseudo-TTY (&lt;code&gt;-t&lt;/code&gt;). The &lt;code&gt;-rm&lt;/code&gt; option means that the container will be removed when you Ctrl+D back out of it. &lt;code&gt;sh&lt;/code&gt; is the command we want to run in the container - using a shell lets us start interacting with it.&lt;/p&gt;
&lt;p&gt;Now that we have a shell against that layer, we can use regular unix commands to start exploring it. &lt;code&gt;du -m&lt;/code&gt; (&lt;code&gt;m&lt;/code&gt; for &lt;code&gt;MB&lt;/code&gt;) is particularly useful here, as it will show us the largest directories in the filesystem. I pipe it through &lt;code&gt;sort&lt;/code&gt; like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ docker run -it --rm abc63755616b sh
# du -m | sort -n
...
58  ./usr/local/lib/python3.6
70  ./usr/local/lib
71  ./usr/local
76  ./usr/lib/python3.5
188 ./usr/lib
306 ./usr
350 .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Straight away we can start seeing where the space is being taken up in our image.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Deleting_unnecessary_files_108"&gt;&lt;/a&gt;Deleting unnecessary files&lt;/h3&gt;
&lt;p&gt;I spent quite a while inspecting different stages of my builds to try and figure out where the space was going. The alpine copy recipe worked neatly, but I was still a little over the limit. When I started to dig around in my final image I spotted some interesting patterns - in particular, the &lt;code&gt;/usr/local/lib/python3.6/site-packages/uvloop&lt;/code&gt; directory was 17MB!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# du -m /usr/local | sort -n -r | head -n 5
96  /usr/local
95  /usr/local/lib
83  /usr/local/lib/python3.6
36  /usr/local/lib/python3.6/site-packages
17  /usr/local/lib/python3.6/site-packages/uvloop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That seems like a lot of disk space for a compiled C module, so I dug in further…&lt;/p&gt;
&lt;p&gt;It turned out the &lt;code&gt;uvloop&lt;/code&gt; folder still contained a bunch of files that were used as part of the compilation, including a 6.7MB &lt;code&gt;loop.c&lt;/code&gt; file and a bunch of &lt;code&gt;.pxd&lt;/code&gt; and &lt;code&gt;.pyd&lt;/code&gt; files that are compiled by &lt;a href="https://cython.org/"&gt;Cython&lt;/a&gt;. None of these files are needed after the extension has been compiled, but they were there, taking up a bunch of precious space.&lt;/p&gt;
&lt;p&gt;So I added the following to my Dockerfile:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RUN find /usr/local/lib/python3.6 -name '*.c' -delete
RUN find /usr/local/lib/python3.6 -name '*.pxd' -delete
RUN find /usr/local/lib/python3.6 -name '*.pyd' -delete
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I noticed that there were &lt;code&gt;__pycache__&lt;/code&gt; files that weren’t needed either, so I added this as well:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RUN find /usr/local/lib/python3.6 -name '__pycache__' | xargs rm -r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(The &lt;code&gt;-delete&lt;/code&gt; flag didn’t work correctly for that one, so I used &lt;code&gt;xargs&lt;/code&gt; instead.)&lt;/p&gt;
&lt;p&gt;This shaved off around 15MB, putting me safely under the limit.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Running_csvstosqlite_in_its_own_stage_137"&gt;&lt;/a&gt;Running csvs-to-sqlite in its own stage&lt;/h3&gt;
&lt;p&gt;The above tricks had got me the smallest Alpine Linux image I could create that would still run Datasette… but Datasette Publish also needs to run &lt;code&gt;csvs-to-sqlite&lt;/code&gt; in order to convert the user’s uploaded CSV files to SQLite.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;csvs-to-sqlite&lt;/code&gt; has some pretty heavy dependencies of its own in the form of &lt;a href="https://pandas.pydata.org/"&gt;Pandas&lt;/a&gt; and &lt;a href="http://www.numpy.org/"&gt;NumPy&lt;/a&gt;. Even with the build chain installed I was having trouble installing these under Alpine, especially since building numpy for Alpine is &lt;a href="https://stackoverflow.com/questions/49037742/why-does-it-take-ages-to-install-pandas-on-alpine-linux"&gt;notoriously slow&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Then I realized that thanks to multi-stage builds there’s no need for me to use Alpine at all for this step. I switched back to &lt;code&gt;python:3.6-slim-stretch&lt;/code&gt; and used it to install &lt;code&gt;csvs-to-sqlite&lt;/code&gt; and compile the CSV files into a SQLite database. I also ran &lt;code&gt;datasette inspect&lt;/code&gt; there for good measure.&lt;/p&gt;
&lt;p&gt;Then in my final Alpine container I could use the following to copy in just those compiled assets:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;COPY --from=csvbuilder inspect-data.json inspect-data.json
COPY --from=csvbuilder data.db data.db
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a id="Tying_it_all_together_150"&gt;&lt;/a&gt;Tying it all together&lt;/h3&gt;
&lt;p&gt;Here’s an example of &lt;a href="https://gist.github.com/simonw/ee63bc5e7feb6e8bb3af82f67a7a36fe"&gt;a full Dockerfile generated by Datasette Publish&lt;/a&gt; that combines all of these tricks. To summarize, here’s what it does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spin up a &lt;code&gt;python:3.6-slim-stretch&lt;/code&gt; - call it &lt;code&gt;csvbuilder&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;apt-get install -y python3-dev gcc&lt;/code&gt; so we can install compiled dependencies&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pip install csvs-to-sqlite datasette&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Copy in the uploaded CSV files&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;csvs-to-sqlite&lt;/code&gt; to convert them into a SQLite database&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;datasette inspect data.db&lt;/code&gt; to generate an &lt;code&gt;inspect-data.json&lt;/code&gt; file with statistics about the tables. This can later be used to reduce startup time for &lt;code&gt;datasette serve&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Spin up a &lt;code&gt;python:3.6-alpine&lt;/code&gt; - call it &lt;code&gt;buildit&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;We need a build chain to compile a copy of datasette for Alpine Linux…&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apk add --no-cache gcc python3-dev musl-dev alpine-sdk&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Now we can &lt;code&gt;pip install datasette&lt;/code&gt;, plus any requested plugins&lt;/li&gt;
&lt;li&gt;Reduce the final image size by deleting any &lt;code&gt;__pycache__&lt;/code&gt; or &lt;code&gt;*.c&lt;/code&gt;, &lt;code&gt;*.pyd&lt;/code&gt; and &lt;code&gt;*.pxd&lt;/code&gt; files.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Spin up a fresh &lt;code&gt;python:3.6-alpine&lt;/code&gt; for our final image
&lt;ul&gt;
&lt;li&gt;Copy in &lt;code&gt;data.db&lt;/code&gt; and &lt;code&gt;inspect-data.json&lt;/code&gt; from &lt;code&gt;csvbuilder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Copy across &lt;code&gt;/usr/local/lib/python3.6&lt;/code&gt; and &lt;code&gt;/usr/local/bin/datasette&lt;/code&gt; from &lt;code&gt;bulidit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;… and we’re done! Expose port 8006 and set &lt;code&gt;datasette serve&lt;/code&gt; to run when the container is started&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now that I’ve finally learned how to take advantage of multi-stage builds I expect I’ll be using them for all sorts of interesting things in the future.&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/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/docker"&gt;docker&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="python"/><category term="zeit-now"/><category term="docker"/><category term="datasette"/></entry><entry><title>The Now CDN</title><link href="https://simonwillison.net/2018/Jul/12/now-cdn/#atom-tag" rel="alternate"/><published>2018-07-12T03:34:06+00:00</published><updated>2018-07-12T03:34:06+00:00</updated><id>https://simonwillison.net/2018/Jul/12/now-cdn/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://zeit.co/blog/now-cdn"&gt;The Now CDN&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Huge announcement from Zeit Now today: all .now.sh deployments are now served through the Cloudflare CDN, which means they benefit from 150 worldwide CDN locations that obey HTTP caching headers. This is particularly relevant for Datasette, since it serves far-future cache headers by default and uses Cloudflare-compatible HTTP/2 push hints to accelerate 302 redirects. This means that both the “datasette publish now” CLI command and the Datasette Publish web app will now result in Cloudflare-accelerated deployments.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cdn"&gt;cdn&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;&lt;/p&gt;



</summary><category term="cdn"/><category term="performance"/><category term="zeit-now"/><category term="datasette"/><category term="cloudflare"/></entry><entry><title>Continuous Integration with Travis CI - ZEIT Documentation</title><link href="https://simonwillison.net/2018/Jun/1/zeit-with-travis-ci/#atom-tag" rel="alternate"/><published>2018-06-01T17:21:50+00:00</published><updated>2018-06-01T17:21:50+00:00</updated><id>https://simonwillison.net/2018/Jun/1/zeit-with-travis-ci/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://zeit.co/docs/continuous-integration/travis?utm_source=twitter&amp;amp;utm_medium=social&amp;amp;utm_campaign=travis_ci_guide"&gt;Continuous Integration with Travis CI - ZEIT Documentation&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
One of the neat things about Zeit Now is that since deployments are unlimited and are automatically assigned a unique URL you can set up a continuous integration system like Travis to deploy a brand new copy of every commit or every pull request. This documentation also shows how to have commits to master automatically aliased to a known URL. I have quite a few Datasette projects that are deployed automatically to Now by Travis and the pattern seems to be working great so far.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/continuous-deployment"&gt;continuous-deployment&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/continuous-integration"&gt;continuous-integration&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/travis"&gt;travis&lt;/a&gt;&lt;/p&gt;



</summary><category term="continuous-deployment"/><category term="continuous-integration"/><category term="zeit-now"/><category term="travis"/></entry><entry><title>Datasette - a talk at Zeit Day SF 2018</title><link href="https://simonwillison.net/2018/Apr/28/datasette/#atom-tag" rel="alternate"/><published>2018-04-28T21:31:40+00:00</published><updated>2018-04-28T21:31:40+00:00</updated><id>https://simonwillison.net/2018/Apr/28/datasette/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://speakerdeck.com/simon/datasette"&gt;Datasette - a talk at Zeit Day SF 2018&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Slides from the talk I gave today about Datasette and Datasette Publish at the Zeit Day SF conference.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/my-talks"&gt;my-talks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;



</summary><category term="my-talks"/><category term="zeit-now"/><category term="datasette"/></entry><entry><title>Make Near Me</title><link href="https://simonwillison.net/2018/Apr/28/make-near-me/#atom-tag" rel="alternate"/><published>2018-04-28T21:28:44+00:00</published><updated>2018-04-28T21:28:44+00:00</updated><id>https://simonwillison.net/2018/Apr/28/make-near-me/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://make-near-me.now.sh/"&gt;Make Near Me&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The natural evolution of owlsnearme.com—Make Near Me uses the Zeit Now API to allow anyone to deploy their own version of Owls Near Me for any species! I announced this on stage at Zeit Day SF 2018 as part of my talk on Datasette and Datasette Publish.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/owlsnearyou"&gt;owlsnearyou&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;&lt;/p&gt;



</summary><category term="owlsnearyou"/><category term="projects"/><category term="zeit-now"/></entry><entry><title>Domains Search for Web: Instant, Serverless &amp; Global</title><link href="https://simonwillison.net/2018/Jan/26/domains-search/#atom-tag" rel="alternate"/><published>2018-01-26T01:14:52+00:00</published><updated>2018-01-26T01:14:52+00:00</updated><id>https://simonwillison.net/2018/Jan/26/domains-search/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://zeit.co/blog/domains-search-web"&gt;Domains Search for Web: Instant, Serverless &amp;amp; Global&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The team at Zeit are pioneering a whole bunch of fascinating web engineering architectural patterns. Their new domain name autocomplete search uses Next.js and server-side rendering on first load, then switches to client-side rendering from then on. It can then load results asynchronously over a custom WebSocket protocol as the microservices on the backend finish resolving domain availability from the various different TLD providers.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/rauchg/status/956402473354366977"&gt;Guillermo Rauch‏&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/domains"&gt;domains&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/websockets"&gt;websockets&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/microservices"&gt;microservices&lt;/a&gt;&lt;/p&gt;



</summary><category term="domains"/><category term="websockets"/><category term="zeit-now"/><category term="microservices"/></entry><entry><title>API 2.0: Log-In with ZEIT, New Docs &amp; More</title><link href="https://simonwillison.net/2018/Jan/17/zeit-api-2/#atom-tag" rel="alternate"/><published>2018-01-17T15:23:15+00:00</published><updated>2018-01-17T15:23:15+00:00</updated><id>https://simonwillison.net/2018/Jan/17/zeit-api-2/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://zeit.co/blog/api-2"&gt;API 2.0: Log-In with ZEIT, New Docs &amp;amp; More&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Here’s Zeit’s write-up of their brand new API 2.0, which adds OAuth support and allows anything that can be done with their command-line tools to be achieved via their public API as well. This is the enabling technology that allowed me to build Datasette Publish.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;&lt;/p&gt;



</summary><category term="zeit-now"/></entry><entry><title>Datasette Publish: a web app for publishing CSV files as an online database</title><link href="https://simonwillison.net/2018/Jan/17/datasette-publish/#atom-tag" rel="alternate"/><published>2018-01-17T14:11:05+00:00</published><updated>2018-01-17T14:11:05+00:00</updated><id>https://simonwillison.net/2018/Jan/17/datasette-publish/#atom-tag</id><summary type="html">
    &lt;p&gt;I’ve just released &lt;a href="https://publish.datasettes.com/"&gt;Datasette Publish&lt;/a&gt;, a web tool for turning one or more CSV files into an online database with a JSON API.&lt;/p&gt;
&lt;p&gt;Here’s &lt;a href="https://datasette-onrlszntsq.now.sh/"&gt;a demo application I built&lt;/a&gt; using Datasette Publish, showing Californian campaign finance data using CSV files released by the &lt;a href="https://www.californiacivicdata.org/"&gt;California Civic Data Coalition&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And here’s an animated screencast showing exactly how I built it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2018/datasette-publish-demo.gif" alt="Animated demo of Datasette Publish" style="max-width: 100%" /&gt;&lt;/p&gt;
&lt;p&gt;Datasette Publish combines my &lt;a href="https://github.com/simonw/datasette"&gt;Datasette&lt;/a&gt; tool for publishing SQLite databases as an API with my &lt;a href="https://github.com/simonw/csvs-to-sqlite"&gt;csvs-to-sqlite&lt;/a&gt; tool for generating them.&lt;/p&gt;
&lt;p&gt;It’s built on top of the &lt;a href="https://zeit.co/now"&gt;Zeit Now&lt;/a&gt; hosting service, which means anything you deploy with it lives on your own account with Zeit and stays entirely under your control. I used the brand new &lt;a href="https://zeit.co/blog/api-2"&gt;Zeit API 2.0&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Zeit’s generous free plan means you can try the tool out as many times as you like - and if you want to use it for an API powering a production website you can easily upgrade to a &lt;a href="https://zeit.co/pricing"&gt;paid hosting plan&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;&lt;a id="Who_should_use_it_16"&gt;&lt;/a&gt;Who should use it&lt;/h2&gt;
&lt;p&gt;Anyone who has data they want to share with the world!&lt;/p&gt;
&lt;p&gt;The fundamental idea behind Datasette is that publishing structured data as both a web interface and a JSON API should be as quick and easy as possible.&lt;/p&gt;
&lt;p&gt;The world is full of interesting data that often ends up trapped in PDF blobs or other hard-to-use formats, if it gets published at all. Datasette encourages using SQLite instead: a powerful, flexible format that enables analysis via SQL queries and can easily be shared and hosted online.&lt;/p&gt;
&lt;p&gt;Since so much of the data that IS published today uses CSV, this first release of Datasette Publish focuses on CSV conversion above anything else. I plan to add support for other useful formats in the future.&lt;/p&gt;
&lt;p&gt;The three areas I’m most excited in seeing adoption of Datasette are data journalism, civic open data and cultural institutions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data journalism&lt;/strong&gt; because when I worked at the Guardian Datasette is the tool I wish I had had for publishing data. When we started &lt;a href="https://www.theguardian.com/data"&gt;the Guardian Datablog&lt;/a&gt; we ended up using Google Sheets for this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Civic open data&lt;/strong&gt; because it turns out the open data movement mostly won! It’s incredible how much high quality data is published by local and national governments these days. My &lt;a href="https://sf-tree-search.now.sh"&gt;San Francisco tree search&lt;/a&gt; project for example uses data from the Department of Public Works - a &lt;a href="https://data.sfgov.org/City-Infrastructure/Street-Tree-List/tkzw-k3nq"&gt;CSV of 190,000 trees&lt;/a&gt; around the city.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cultural institutions&lt;/strong&gt; because the museums and libraries of the world are sitting on enormous treasure troves of valuable information, and have an institutional mandate to share that data as widely as possible.&lt;/p&gt;
&lt;p&gt;If you are involved in any of the above please &lt;a href="https://twitter.com/simonw"&gt;get in touch&lt;/a&gt;. I’d love your help improving the Datasette ecosystem to better serve your needs.&lt;/p&gt;
&lt;h2&gt;&lt;a id="How_it_works_36"&gt;&lt;/a&gt;How it works&lt;/h2&gt;
&lt;p&gt;Datasette Publish would not be possible without Zeit Now. Now is a revolutionary approach to hosting: it lets you instantly create immutable deployments with a unique URL, via a command-line tool or using &lt;a href="https://zeit.co/api"&gt;their recently updated API&lt;/a&gt;. It’s by far the most productive hosting environment I’ve ever worked with.&lt;/p&gt;
&lt;p&gt;I built the main Datasette Publish interface using React. Building a SPA here made a lot of sense, because it allowed me to construct the entire application without any form of server-side storage (aside from &lt;a href="https://keen.io/"&gt;Keen&lt;/a&gt; for analytics).&lt;/p&gt;
&lt;p&gt;When you sign in via Zeit OAuth I store your access token in a signed cookie. Each time you upload a CSV the file is stored directly using Zeit’s upload API, and the file metadata is persisted in JavaScript state in the React app. When you click “publish” the accumulated state is sent to the server where it is used to construct a new Zeit deployment.&lt;/p&gt;
&lt;p&gt;The deployment itself consists of the CSV files plus &lt;a href="https://gist.github.com/simonw/365294fb51765fb07bc99fe5eb7fee22"&gt;a Dockerfile&lt;/a&gt; that installs Python, Datasette, csvs-to-sqlite and their dependencies, then runs csvs-to-sqlite against the CSV files and starts up Datasette against the resulting database.&lt;/p&gt;
&lt;p&gt;If you specified a title, description, source or license I generate a Datasette &lt;a href="https://datasette.readthedocs.io/en/latest/metadata.html"&gt;metadata.json&lt;/a&gt; file and include that in the deployment as well.&lt;/p&gt;
&lt;p&gt;Since free deployments to Zeit are “source code visible”, you can see exactly how the resulting application is structured by visiting &lt;a href="https://datasette-onrlszntsq.now.sh/_src"&gt;https://datasette-onrlszntsq.now.sh/_src&lt;/a&gt; (the campaign finance app I built earlier).&lt;/p&gt;
&lt;p&gt;Using the Zeit API in this way has the neat effect that I don’t ever store any user data myself - neither the access token used to access your account nor any of the CSVs that you upload. Uploaded files go straight to your own Zeit account and stay under your control. Access tokens are never persisted. The deployed application lives on your own hosting account, where you can terminate it or upgrade it to a paid plan without any further involvement from the tool I have built.&lt;/p&gt;
&lt;p&gt;Not having to worry about storing encrypted access tokens or covering any hosting costs beyond the Datasette Publish tool itself is delightful.&lt;/p&gt;
&lt;p&gt;This ability to build tools that themselves deploy other tools is fascinating. I can’t wait to see what other kinds of interesting new applications it enables.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://news.ycombinator.com/item?id=16170892"&gt;Discussion on Hacker News&lt;/a&gt;.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/csv"&gt;csv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="csv"/><category term="projects"/><category term="zeit-now"/><category term="datasette"/></entry><entry><title>ftfy - fix unicode that's broken in various ways</title><link href="https://simonwillison.net/2018/Jan/9/ftfy/#atom-tag" rel="alternate"/><published>2018-01-09T03:22:25+00:00</published><updated>2018-01-09T03:22:25+00:00</updated><id>https://simonwillison.net/2018/Jan/9/ftfy/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://ftfy.now.sh/"&gt;ftfy - fix unicode that&amp;#x27;s broken in various ways&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I shipped a small web UI wrapper around the excellent Python FTFY library, which can take broken unicode strings and suggest a sequence of operations that can be applied to get back sensible text.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/simonw/status/950565555873837056"&gt;Me on Twitter&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/unicode"&gt;unicode&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;&lt;/p&gt;



</summary><category term="projects"/><category term="unicode"/><category term="zeit-now"/></entry><entry><title>gzthermal-web</title><link href="https://simonwillison.net/2017/Nov/21/gzthermal-web/#atom-tag" rel="alternate"/><published>2017-11-21T18:24:12+00:00</published><updated>2017-11-21T18:24:12+00:00</updated><id>https://simonwillison.net/2017/Nov/21/gzthermal-web/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://gzthermal.now.sh/"&gt;gzthermal-web&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I built a quick web application wrapping the &lt;code&gt;gzthermal&lt;/code&gt; gzip visualization tool and deployed it to Zeit Now wrapped up in a Docker container. Give it a URL and it shows you a PNG visualization of how gzip encodes that page.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://github.com/simonw/gzthermal-web"&gt;GitHub&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/sanic"&gt;sanic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/docker"&gt;docker&lt;/a&gt;&lt;/p&gt;



</summary><category term="projects"/><category term="sanic"/><category term="zeit-now"/><category term="docker"/></entry><entry><title>now-ab</title><link href="https://simonwillison.net/2017/Nov/16/now-ab/#atom-tag" rel="alternate"/><published>2017-11-16T23:03:55+00:00</published><updated>2017-11-16T23:03:55+00:00</updated><id>https://simonwillison.net/2017/Nov/16/now-ab/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/sergiodxa/now-ab"&gt;now-ab&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Intriguing example of a Zeit Now microservice: now-ab is a Node.js HTTP proxy which proxies through to one of two or more other Now-deployed applications based on a cookie. If you don’t have the cookie, it picks a backend at random and sets the cookie. Admittedly this is the easiest part of implementing A/B testing (the hard part is the analytics: tracking exposures and conversions) but as an example of a microservice architectural pattern this is fascinating.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/ab-testing"&gt;ab-testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/nodejs"&gt;nodejs&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/microservices"&gt;microservices&lt;/a&gt;&lt;/p&gt;



</summary><category term="ab-testing"/><category term="nodejs"/><category term="zeit-now"/><category term="microservices"/></entry><entry><title>ZEIT – 6x Faster Now Uploads with HTTP/2</title><link href="https://simonwillison.net/2017/Nov/8/zeit-http2/#atom-tag" rel="alternate"/><published>2017-11-08T01:04:56+00:00</published><updated>2017-11-08T01:04:56+00:00</updated><id>https://simonwillison.net/2017/Nov/8/zeit-http2/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://zeit.co/blog/http2-uploads"&gt;ZEIT – 6x Faster Now Uploads with HTTP/2&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Fantastic optimization write-up by Pranay Prakash. The Now deployment tool works by computing a hash for every local file in a project, then uploading just the ones that are missing. Pranay switched to uploading over HTTP/2 using the fetch-h2 library and got a 6x speedup for larger projects.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/rauchg/status/928045524430635008"&gt;Guillermo Rauch&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/nodejs"&gt;nodejs&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http2"&gt;http2&lt;/a&gt;&lt;/p&gt;



</summary><category term="nodejs"/><category term="zeit-now"/><category term="http2"/></entry><entry><title>Running a load testing Go utility using Docker for Mac</title><link href="https://simonwillison.net/2017/Nov/5/golang-docker-for-mac/#atom-tag" rel="alternate"/><published>2017-11-05T03:50:20+00:00</published><updated>2017-11-05T03:50:20+00:00</updated><id>https://simonwillison.net/2017/Nov/5/golang-docker-for-mac/#atom-tag</id><summary type="html">
    &lt;p&gt;I’m playing around with &lt;a href="https://zeit.co/now"&gt;Zeit Now&lt;/a&gt; at the moment (see &lt;a href="https://simonwillison.net/2017/Oct/14/async-python-sanic-now/"&gt;my previous entry&lt;/a&gt;) and decided to hit it with some traffic using Apache Bench. I got this SSL handshake error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;simonw$ ab -n 10 -c 2 'https://json-head.now.sh/'
This is ApacheBench, Version 2.3 &amp;lt;$Revision: 1706008 $&amp;gt;
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking json-head.now.sh (be patient)...SSL handshake failed (1).
140735278280784:error:14077438:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert internal error:/Library/Caches/com.apple.xbs/Sources/libressl/libressl-1.60.1.2.1/libressl/ssl/s23_clnt.c:541:
SSL handshake failed (1).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some brief Googling turned up &lt;a href="https://stackoverflow.com/questions/38516969/apache-benchmark-https-issue"&gt;this thread on Stack Overflow&lt;/a&gt;, which suggested trying &lt;a href="https://github.com/rakyll/hey"&gt;hey&lt;/a&gt; as an alternative. Hey is a load testing utility written in Go, and the installation instructions are as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go get -u github.com/rakyll/hey
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unfortunately, I don’t have a current Go environment set up on this laptop - I have Go 1.6, but Hey calls for at least Go 1.7.&lt;/p&gt;
&lt;p&gt;Rather than work through upgrading my Go environment, I decided to see if I could get this tool working using &lt;a href="https://www.docker.com/docker-mac"&gt;Docker for Mac&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We recently switched to Docker for Mac for running our development environments at work, and having worked through various iterations of Docker over the past few years Docker for Mac offers by far the most pleasant developer experience. You download the installer, run it, and now &lt;code&gt;docker info&lt;/code&gt; in a terminal will reveal a fully functioning Docker environment. Couldn’t be simpler.&lt;/p&gt;
&lt;p&gt;But how to use it to run a one-off tool written in Go? &lt;a href="https://blog.docker.com/2016/09/docker-golang/"&gt;This article on the official Docker blog&lt;/a&gt; gave me everything I needed to know.&lt;/p&gt;
&lt;p&gt;First step: run the &lt;code&gt;go get&lt;/code&gt; command in a brand new Docker container, like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run golang go get -v github.com/rakyll/hey
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This runs the &lt;code&gt;go get&lt;/code&gt; command in a new instance of the &lt;a href="https://hub.docker.com/_/golang/"&gt;official golang container&lt;/a&gt;. If you’ve never used the container before, Docker will download everything it needs before executing the rest of the command.&lt;/p&gt;
&lt;p&gt;Once this command finishes, we have a container with the Go program compiled and installed in it. But how to run it?&lt;/p&gt;
&lt;p&gt;We can “commit” the container to freeze it into a new image that bakes in the command. Here’s how to do that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker commit $(docker ps -lq) heyimage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The nested &lt;code&gt;docker ps -lq&lt;/code&gt; command outputs the container ID. The outer &lt;code&gt;docker commit&lt;/code&gt; command then creates a new image freezing those latest changes.&lt;/p&gt;
&lt;p&gt;Having frozen the container, we can run the command like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run heyimage hey -n 10 -c 2 'https://json-head.now.sh/'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the command runs, exactly as if I’d installed it without using Docker at all.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;simonw$ docker run heyimage hey -n 10 -c 2 'https://json-head.now.sh/'
Summary:
  Total:    0.9778 secs
  Slowest:  0.6794 secs
  Fastest:  0.0564 secs
  Average:  0.1954 secs
  Requests/sec: 10.2266

Response time histogram:
  0.056 [1] |∎∎∎∎∎∎
  0.119 [7] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  0.181 [0] |
  0.243 [0] |
  0.306 [0] |
  0.368 [0] |
  0.430 [0] |
  0.493 [0] |
  0.555 [0] |
  0.617 [0] |
  0.679 [2] |∎∎∎∎∎∎∎∎∎∎∎

Latency distribution:
  10% in 0.0588 secs
  25% in 0.0653 secs
  50% in 0.0868 secs
  75% in 0.6792 secs
  90% in 0.6794 secs

Details (average, fastest, slowest):
  DNS+dialup:    0.1221 secs, 0.0000 secs, 0.6109 secs
  DNS-lookup:    0.0981 secs, 0.0000 secs, 0.4906 secs
  req write:     0.0001 secs, 0.0000 secs, 0.0001 secs
  resp wait:     0.0727 secs, 0.0561 secs, 0.0904 secs
  resp read:     0.0004 secs, 0.0001 secs, 0.0012 secs

Status code distribution:
  [200] 10 responses
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One last puzzle: the above command worked for load testing externally hosted URLs, but I also wanted to try running it against a web server running on port 8000 on my Mac itself. Running &lt;code&gt;hey&lt;/code&gt; against &lt;code&gt;http://localhost:8000/&lt;/code&gt; didn't work inside the container. Instead, I ran &lt;code&gt;ipconfig getifaddr en0&lt;/code&gt; to find the local network IP address of my Mac and then ran &lt;code&gt;hey&lt;/code&gt; against that IP address (&lt;a href="https://stackoverflow.com/questions/22944631/how-to-get-the-ip-address-of-the-docker-host-from-inside-a-docker-container"&gt;thanks again, Stack Overflow&lt;/a&gt;):&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;simonw$ docker run heyimage hey -n 100 -c 10 'http://10.0.0.12:8000/'
Summary:
  Total:	0.2481 secs
  ...&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;For me, this use-case illustrates a huge part of the value of Docker: it lets you execute tools written in basically anything without having to pollute your laptop with environment junk.&lt;/p&gt;

&lt;h2&gt;&lt;a id="Running_commands_against_files_83"&gt;&lt;/a&gt;Running commands against files&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Update: 9th November 2017&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I decided to use this technique to try out &lt;a href="https://github.com/tdewolff/minify/tree/master/cmd/minify"&gt;this Go minify tool&lt;/a&gt; by Taco de Wolff. Building the tool into a container used the same pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run golang go get -v github.com/tdewolff/minify/cmd/minify
docker commit $(docker ps -lq) minify
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running the command this time is a bit harder, because it needs access to files on my filesystem. I can give it access by mounting my current directory as part of the &lt;code&gt;docker run&lt;/code&gt; command, like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -v `pwd`:/mnt minify minify /mnt/all.css
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running this minifies the contents of the &lt;code&gt;all.css&lt;/code&gt; file in my current directory and outputs the result to standard out. If I want to save it I can redirect it to a file like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -v `pwd`:/mnt minify minify /mnt/all.css &amp;gt; all.min.css
&lt;/code&gt;&lt;/pre&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/go"&gt;go&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/load-testing"&gt;load-testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/docker"&gt;docker&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="go"/><category term="load-testing"/><category term="zeit-now"/><category term="docker"/></entry><entry><title>Carbon</title><link href="https://simonwillison.net/2017/Oct/19/carbon/#atom-tag" rel="alternate"/><published>2017-10-19T18:31:47+00:00</published><updated>2017-10-19T18:31:47+00:00</updated><id>https://simonwillison.net/2017/Oct/19/carbon/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://carbon.now.sh/"&gt;Carbon&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Beautiful little tool that you can paste source code into to generate an image of that code with syntax highlighting applied, ready to be tweeted or shared anywhere that lets you share an image. Built in Node and next.js, with image generation handled client-side by the dom-to-image JavaScript library which loads HTML into a SVG foreignObject (sadly not yet supported by Safari) and uses that to populate a canvas and produce a PNG.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/rauchg/status/921031246385262592"&gt;Guillermo Rauch&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/nodejs"&gt;nodejs&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/svg"&gt;svg&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;&lt;/p&gt;



</summary><category term="javascript"/><category term="nodejs"/><category term="svg"/><category term="zeit-now"/></entry><entry><title>Deploying an asynchronous Python microservice with Sanic and Zeit Now</title><link href="https://simonwillison.net/2017/Oct/14/async-python-sanic-now/#atom-tag" rel="alternate"/><published>2017-10-14T21:46:38+00:00</published><updated>2017-10-14T21:46:38+00:00</updated><id>https://simonwillison.net/2017/Oct/14/async-python-sanic-now/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;a href="https://simonwillison.net/tags/jsonhead/"&gt;Back in 2008&lt;/a&gt; Natalie Downe and I deployed what today we would call a microservice: &lt;a href="https://github.com/simonw/json-head"&gt;json-head&lt;/a&gt;, a tiny Google App Engine app that allowed you to make an HTTP head request against a URL and get back the HTTP headers as JSON. One of our initial use-scase for this was &lt;a href="https://gist.github.com/natbat/8406b8e5a8ed22d6a2e1bbd75771bc97"&gt;Natalie’s addSizes.js&lt;/a&gt;, an unobtrusive jQuery script that could annotate links to PDFs and other large files with their corresponding file size pulled from the &lt;code&gt;Content-Length&lt;/code&gt; header. Another potential use-case is detecting broken links, since the API can be used to spot 404 status codes (&lt;a href="https://json-head.now.sh/?url=https://simonwillison.net/page-does-not-exist"&gt;as in this example&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;At some point in the following decade &lt;code&gt;json-head.appspot.com&lt;/code&gt; stopped working. Today I’m bringing it back, mainly as an excuse to try out the combination of Python 3.5 async, the &lt;a href="https://github.com/channelcat/sanic/"&gt;Sanic&lt;/a&gt; microframework and Zeit’s brilliant &lt;a href="https://zeit.co/now"&gt;Now&lt;/a&gt; deployment platform.&lt;/p&gt;
&lt;p&gt;First, a demo. &lt;a href="https://json-head.now.sh/?url=https://simonwillison.net/"&gt;https://json-head.now.sh/?url=https://simonwillison.net/&lt;/a&gt; returns the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
    {
        &amp;quot;ok&amp;quot;: true,
        &amp;quot;headers&amp;quot;: {
            &amp;quot;Date&amp;quot;: &amp;quot;Sat, 14 Oct 2017 18:37:52 GMT&amp;quot;,
            &amp;quot;Content-Type&amp;quot;: &amp;quot;text/html; charset=utf-8&amp;quot;,
            &amp;quot;Connection&amp;quot;: &amp;quot;keep-alive&amp;quot;,
            &amp;quot;Set-Cookie&amp;quot;: &amp;quot;__cfduid=dd0b71b4e89bbaca5b27fa06c0b95af4a1508006272; expires=Sun, 14-Oct-18 18:37:52 GMT; path=/; domain=.simonwillison.net; HttpOnly; Secure&amp;quot;,
            &amp;quot;Cache-Control&amp;quot;: &amp;quot;s-maxage=200&amp;quot;,
            &amp;quot;X-Frame-Options&amp;quot;: &amp;quot;SAMEORIGIN&amp;quot;,
            &amp;quot;Via&amp;quot;: &amp;quot;1.1 vegur&amp;quot;,
            &amp;quot;CF-Cache-Status&amp;quot;: &amp;quot;HIT&amp;quot;,
            &amp;quot;Vary&amp;quot;: &amp;quot;Accept-Encoding&amp;quot;,
            &amp;quot;Server&amp;quot;: &amp;quot;cloudflare-nginx&amp;quot;,
            &amp;quot;CF-RAY&amp;quot;: &amp;quot;3adca70269a51e8f-SJC&amp;quot;,
            &amp;quot;Content-Encoding&amp;quot;: &amp;quot;gzip&amp;quot;
        },
        &amp;quot;status&amp;quot;: 200,
        &amp;quot;url&amp;quot;: &amp;quot;https://simonwillison.net/&amp;quot;
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Given a URL, &lt;code&gt;json-head.now.sh&lt;/code&gt; performs an HTTP HEAD request and returns the resulting status code and the HTTP headers. Results are returned with the &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; header so you can call the API using &lt;code&gt;fetch()&lt;/code&gt; or &lt;code&gt;XMLHttpRequest&lt;/code&gt; from JavaScript running on any page.&lt;/p&gt;
&lt;h2&gt;&lt;a id="Sanic_and_Python_asyncawait_32"&gt;&lt;/a&gt;Sanic and Python async/await&lt;/h2&gt;
&lt;p&gt;A key new feature &lt;a href="https://docs.python.org/3/whatsnew/3.5.html"&gt;added to Python 3.5&lt;/a&gt; back in September 2015 was built-in syntactic support for coroutine control via the async/await statements. Python now has some serious credibility as a platform for asynchronous I/O (the concept that got me &lt;a href="https://simonwillison.net/2009/Nov/23/node/"&gt;so excited about Node.js back in 2009&lt;/a&gt;). This has lead to an explosion of asynchronous innovation around the Python community.&lt;/p&gt;
&lt;p&gt;json-head is the perfect application for async - it’s little more than a dumbed-down HTTP proxy, accepting incoming HTTP requests, making its own requests elsewhere and then returning the results.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/channelcat/sanic/"&gt;Sanic&lt;/a&gt; is a Flask-like web framework built specifically to take advantage of async/await in Python 3.5. It’s designed for speed - built on top of &lt;a href="https://github.com/MagicStack/uvloop"&gt;uvloop&lt;/a&gt;, a Python wrapper for &lt;a href="https://github.com/libuv/libuv"&gt;libuv&lt;/a&gt; (which itself was originally built to power Node.js). uvloop’s self-selected benchmarks are &lt;a href="https://magic.io/blog/uvloop-blazing-fast-python-networking/"&gt;extremely impressive&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;&lt;a id="Zeit_Now_40"&gt;&lt;/a&gt;Zeit Now&lt;/h2&gt;
&lt;p&gt;To host this new microservice, I chose &lt;a href="https://zeit.co/now"&gt;Zeit Now&lt;/a&gt;. It’s a truly beautiful piece of software design.&lt;/p&gt;
&lt;p&gt;Now lets you treat deployments as immutable. Every time you deploy you get a brand new URL. You can then interact with your deployment directly, or point an existing alias to it if you want a persistent URL for your project.&lt;/p&gt;
&lt;p&gt;Deployments are free, and deployed code stays available forever due to &lt;a href="https://github.com/zeit/now-cli/issues/189"&gt;some clever engineering&lt;/a&gt; behind the scenes.&lt;/p&gt;
&lt;p&gt;Best of all: deploying a project takes just a single command: type &lt;code&gt;now&lt;/code&gt; and the code in your current directory will be deployed to their cloud and assigned a unique URL.&lt;/p&gt;
&lt;p&gt;Now was originally built for Node.js projects, but last August &lt;a href="https://zeit.co/blog/now-dockerfile"&gt;Zeit added Docker support&lt;/a&gt;. If the directory you run it in contains a Dockerfile, running &lt;code&gt;now&lt;/code&gt; will upload, build and run the corresponding image.&lt;/p&gt;
&lt;p&gt;There’s just one thing missing: good examples of how to deploy Python projects to Now using Docker. I’m hoping this article can help fill that gap.&lt;/p&gt;
&lt;p&gt;Here’s the &lt;a href="https://github.com/simonw/json-head/blob/master/Dockerfile"&gt;complete Dockerfile&lt;/a&gt; I’m using for json-head:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM python:3
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
EXPOSE 8006
CMD [&amp;quot;python&amp;quot;, &amp;quot;json_head.py&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I’m using the &lt;a href="https://hub.docker.com/_/python/"&gt;official Docker Python image&lt;/a&gt; as a base, copying the current directory into the image, using &lt;code&gt;pip install&lt;/code&gt; to install dependencies and then exposing port 8006 (for no reason other than that’s the port I use for local development environment) and running the &lt;a href="https://github.com/simonw/json-head/blob/master/json_head.py"&gt;json_head.py&lt;/a&gt; script. Now is smart enough to forward incoming HTTP traffic on port 80 to the port that was exposed by the container.&lt;/p&gt;
&lt;p&gt;If you setup Now yourself (&lt;code&gt;npm install -g now&lt;/code&gt; or use &lt;a href="https://zeit.co/download"&gt;one of their installers&lt;/a&gt;) you can deploy my code directly from GitHub to your own instance with a single command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ now simonw/json-head
&amp;gt; Didn't find directory. Searching on GitHub...
&amp;gt; Deploying GitHub repository &amp;quot;simonw/json-head&amp;quot; under simonw
&amp;gt; Ready! https://simonw-json-head-xqkfgorgei.now.sh (copied to clipboard) [1s]
&amp;gt; Initializing…
&amp;gt; Building
&amp;gt; ▲ docker build
Sending build context to Docker daemon 7.168 kBkB
&amp;gt; Step 1 : FROM python:3
&amp;gt; 3: Pulling from library/python
&amp;gt; ... lots more stuff here ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a id="Initial_implementation_79"&gt;&lt;/a&gt;Initial implementation&lt;/h2&gt;
&lt;p&gt;Here’s my first working version of json-head using Sanic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from sanic import Sanic
from sanic import response
import aiohttp

app = Sanic(__name__)

async def head(session, url):
    try:
        async with session.head(url) as response:
            return {
                'ok': True,
                'headers': dict(response.headers),
                'status': response.status,
                'url': url,
            }
    except Exception as e:
        return {
            'ok': False,
            'error': str(e),
            'url': url,
        }

@app.route('/')
async def handle_request(request):
    url = request.args.get('url')
    if url:
        async with aiohttp.ClientSession() as session:
            head_info = await head(session, url)
            return response.json(
                head_info,
                headers={
                    'Access-Control-Allow-Origin': '*'
                },
            )
    else:
        return response.html('Try /?url=xxx')

if __name__ == '__main__':
    app.run(host=&amp;quot;0.0.0.0&amp;quot;, port=8006)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This exact code is deployed at &lt;a href="https://json-head-thlbstmwfi.now.sh/"&gt;https://json-head-thlbstmwfi.now.sh/&lt;/a&gt; - since Now deployments are free, there’s no reason not to leave work-in-progress examples hosted as throwaway deployments.&lt;/p&gt;
&lt;p&gt;In addition to Sanic, I’m also using the handy &lt;a href="https://github.com/aio-libs/aiohttp"&gt;aiohttp&lt;/a&gt; asynchronous HTTP library - which features API design clearly inspired by my all-time favourite HTTP library, &lt;a href="https://github.com/requests/requests"&gt;requests&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The key new pieces of syntax to understand in the above code are the async and await statements. &lt;code&gt;async def&lt;/code&gt; is used to declare a function that acts as a coroutine. Coroutines need to be executed inside an event loop (which Sanic handles for us), but gain the ability to use the &lt;code&gt;await&lt;/code&gt; statement.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;await&lt;/code&gt; statement is the real magic here: it suspends the current coroutine until the coroutine it is calling has finished executing. It is this that allows us to write asynchronous code without descending into a messy hell of callback functions.&lt;/p&gt;
&lt;h2&gt;&lt;a id="Adding_parallel_requests_131"&gt;&lt;/a&gt;Adding parallel requests&lt;/h2&gt;
&lt;p&gt;So far we haven’t really taken advantage of what async I/O can do - if every incoming HTTP request results in a single outgoing HTTP response then async may help us scale to serve more incoming requests at once but it’s not really giving us any new functionality.&lt;/p&gt;
&lt;p&gt;Executing multiple outbound HTTP requests in parallel is a much more interesting use-case. Let’s add support for multiple &lt;code&gt;?url=&lt;/code&gt; parameters, such as the following:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://json-head.now.sh/?url=https://simonwillison.net/&amp;amp;url=https://www.google.com/"&gt;https://json-head.now.sh/?url=https://simonwillison.net/&amp;amp;url=https://www.google.com/&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
    {
        &amp;quot;ok&amp;quot;: true,
        &amp;quot;headers&amp;quot;: {
            &amp;quot;Date&amp;quot;: &amp;quot;Sat, 14 Oct 2017 19:35:29 GMT&amp;quot;,
            &amp;quot;Content-Type&amp;quot;: &amp;quot;text/html; charset=utf-8&amp;quot;,
            &amp;quot;Connection&amp;quot;: &amp;quot;keep-alive&amp;quot;,
            &amp;quot;Set-Cookie&amp;quot;: &amp;quot;__cfduid=ded486c1faaac166e8ae72a87979c02101508009729; expires=Sun, 14-Oct-18 19:35:29 GMT; path=/; domain=.simonwillison.net; HttpOnly; Secure&amp;quot;,
            &amp;quot;Cache-Control&amp;quot;: &amp;quot;s-maxage=200&amp;quot;,
            &amp;quot;X-Frame-Options&amp;quot;: &amp;quot;SAMEORIGIN&amp;quot;,
            &amp;quot;Via&amp;quot;: &amp;quot;1.1 vegur&amp;quot;,
            &amp;quot;CF-Cache-Status&amp;quot;: &amp;quot;EXPIRED&amp;quot;,
            &amp;quot;Vary&amp;quot;: &amp;quot;Accept-Encoding&amp;quot;,
            &amp;quot;Server&amp;quot;: &amp;quot;cloudflare-nginx&amp;quot;,
            &amp;quot;CF-RAY&amp;quot;: &amp;quot;3adcfb671c862888-SJC&amp;quot;,
            &amp;quot;Content-Encoding&amp;quot;: &amp;quot;gzip&amp;quot;
        },
        &amp;quot;status&amp;quot;: 200,
        &amp;quot;url&amp;quot;: &amp;quot;https://simonwillison.net/&amp;quot;
    },
    {
        &amp;quot;ok&amp;quot;: true,
        &amp;quot;headers&amp;quot;: {
            &amp;quot;Date&amp;quot;: &amp;quot;Sat, 14 Oct 2017 19:35:29 GMT&amp;quot;,
            &amp;quot;Expires&amp;quot;: &amp;quot;-1&amp;quot;,
            &amp;quot;Cache-Control&amp;quot;: &amp;quot;private, max-age=0&amp;quot;,
            &amp;quot;Content-Type&amp;quot;: &amp;quot;text/html; charset=ISO-8859-1&amp;quot;,
            &amp;quot;P3P&amp;quot;: &amp;quot;CP=\&amp;quot;This is not a P3P policy! See g.co/p3phelp for more info.\&amp;quot;&amp;quot;,
            &amp;quot;Content-Encoding&amp;quot;: &amp;quot;gzip&amp;quot;,
            &amp;quot;Server&amp;quot;: &amp;quot;gws&amp;quot;,
            &amp;quot;X-XSS-Protection&amp;quot;: &amp;quot;1; mode=block&amp;quot;,
            &amp;quot;X-Frame-Options&amp;quot;: &amp;quot;SAMEORIGIN&amp;quot;,
            &amp;quot;Set-Cookie&amp;quot;: &amp;quot;1P_JAR=2017-10-14-19; expires=Sat, 21-Oct-2017 19:35:29 GMT; path=/; domain=.google.com&amp;quot;,
            &amp;quot;Alt-Svc&amp;quot;: &amp;quot;quic=\&amp;quot;:443\&amp;quot;; ma=2592000; v=\&amp;quot;39,38,37,35\&amp;quot;&amp;quot;,
            &amp;quot;Transfer-Encoding&amp;quot;: &amp;quot;chunked&amp;quot;
        },
        &amp;quot;status&amp;quot;: 200,
        &amp;quot;url&amp;quot;: &amp;quot;https://www.google.com/&amp;quot;
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We’re now accepting multiple URLs and executing multiple HEAD requests… but Python 3.5 async makes it easy to do this in parallel, so our overall request time should match that of the single longest HEAD request that we triggered.&lt;/p&gt;
&lt;p&gt;Here’s an implementation that adds support for multiple, parallel outbound HTTP requests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@app.route('/')
async def handle_request(request):
    urls = request.args.getlist('url')
    if urls:
        async with aiohttp.ClientSession() as session:
            head_infos = await asyncio.gather(*[
                head(session, url) for url in urls
            ])
            return response.json(
                head_infos,
                headers={'Access-Control-Allow-Origin': '*'},
            )
    else:
        return response.html(INDEX)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We’re using the &lt;code&gt;asyncio&lt;/code&gt; module from the Python 3.5 standard library here - in particular the &lt;code&gt;gather&lt;/code&gt; function. &lt;a href="https://docs.python.org/3/library/asyncio-task.html#asyncio.gather"&gt;&lt;code&gt;asyncio.gather&lt;/code&gt;&lt;/a&gt; takes a list of coroutines and returns a future aggregating their results. This future will resolve (and return to a corresponding &lt;code&gt;await&lt;/code&gt; statement) as soon as all of those coroutines have returned their values.&lt;/p&gt;
&lt;p&gt;My final code for json-head &lt;a href="https://github.com/simonw/json-head"&gt;can be found on GitHub&lt;/a&gt;. As I hope I’ve demonstrated, the combination of Python 3.5+, Sanic and Now makes deploying asynchronous Python microservices trivially easy.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jsonhead"&gt;jsonhead&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/natalie-downe"&gt;natalie-downe&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sanic"&gt;sanic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zeit-now"&gt;zeit-now&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/docker"&gt;docker&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="async"/><category term="jsonhead"/><category term="natalie-downe"/><category term="python"/><category term="sanic"/><category term="zeit-now"/><category term="docker"/></entry></feed>