<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: sanic</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/sanic.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2017-11-21T18:24:12+00:00</updated><author><name>Simon Willison</name></author><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>Datasette: instantly create and publish an API for your SQLite databases</title><link href="https://simonwillison.net/2017/Nov/13/datasette/#atom-tag" rel="alternate"/><published>2017-11-13T23:49:28+00:00</published><updated>2017-11-13T23:49:28+00:00</updated><id>https://simonwillison.net/2017/Nov/13/datasette/#atom-tag</id><summary type="html">
    &lt;p&gt;I just shipped the first public version of &lt;a href="https://github.com/simonw/datasette"&gt;datasette&lt;/a&gt;, a new tool for creating and publishing JSON APIs for SQLite databases.&lt;/p&gt;
&lt;p&gt;You can try out out right now at &lt;a href="https://fivethirtyeight.datasettes.com/"&gt;fivethirtyeight.datasettes.com&lt;/a&gt;, where you can explore SQLite databases I built from Creative Commons licensed CSV files &lt;a href="https://github.com/fivethirtyeight/data"&gt;published by FiveThirtyEight&lt;/a&gt;. Or you can check out &lt;a href="https://parlgov.datasettes.com/"&gt;parlgov.datasettes.com&lt;/a&gt;, derived from the &lt;a href="http://www.parlgov.org/"&gt;parlgov.org&lt;/a&gt; database of world political parties which illustrates some advanced features such as &lt;a href="https://parlgov.datasettes.com/parlgov-25f9855/view_party"&gt;SQLite views&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fivethirtyeight.datasettes.com/fivethirtyeight/most-common-name%2Fsurnames"&gt;&lt;img alt="Common surnames from fivethirtyeight" src="https://static.simonwillison.net/static/2017/fivethirtyeight-surnames.png"  style="width: 100%" /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Or you can try it out on your own machine. If you run OS X and use Google Chrome, try running the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip3 install datasette
datasette ~/Library/Application\ Support/Google/Chrome/Default/History
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will start a web server on &lt;a href="http://127.0.0.1:8001/"&gt;http://127.0.0.1:8001/&lt;/a&gt; displaying an interface that will let you browse your Chrome browser history, which is conveniently stored in a SQLite database.&lt;/p&gt;
&lt;p&gt;Got a SQLite database you want to share with the world? Provided you have &lt;a href="https://zeit.co/now"&gt;Zeit Now&lt;/a&gt; set up on your machine, you can publish one or more databases with a single command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette publish now my-database.db
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above command will whir away for about a minute and then spit out a URL to a hosted version of datasette with your database (or databases) ready to go. This is how I’m hosting the fivethirtyeight and parlgov example datasets, albeit on a custom domain behind a &lt;a href="https://cloudflare.com/"&gt;Cloudflare&lt;/a&gt; cache.&lt;/p&gt;
&lt;h2&gt;&lt;a id="The_datasette_API_19"&gt;&lt;/a&gt;The datasette API&lt;/h2&gt;
&lt;p&gt;Everything datasette can do is driven by URLs. Queries can produce responsive HTML pages (I’m using a variant of &lt;a href="https://css-tricks.com/responsive-data-tables/"&gt;this responsive tables pattern&lt;/a&gt; for smaller screens) or with the &lt;code&gt;.json&lt;/code&gt; or &lt;code&gt;.jsono&lt;/code&gt; extension can produce JSON. All JSON responses are served with an &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; HTTP header, meaning you can query them from any page.&lt;/p&gt;
&lt;p&gt;You can try that right now in your browser’s developer console. Navigate to &lt;a href="http://www.example.com/"&gt;http://www.example.com/&lt;/a&gt; and enter the following in the console:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fetch(
    &amp;quot;https://fivethirtyeight.datasettes.com/fivethirtyeight-2628db9/avengers%2Favengers.jsono&amp;quot;
).then(
    r =&amp;gt; r.json()
).then(data =&amp;gt; console.log(
    JSON.stringify(data.rows[0], null, '  ')
))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You’ll see the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;rowid&amp;quot;: 1,
  &amp;quot;URL&amp;quot;: &amp;quot;http://marvel.wikia.com/Henry_Pym_(Earth-616)&amp;quot;,
  &amp;quot;Name/Alias&amp;quot;: &amp;quot;Henry Jonathan \&amp;quot;Hank\&amp;quot; Pym&amp;quot;,
  &amp;quot;Appearances&amp;quot;: 1269,
  &amp;quot;Gender&amp;quot;: &amp;quot;MALE&amp;quot;,
  &amp;quot;Full/Reserve Avengers Intro&amp;quot;: &amp;quot;Sep-63&amp;quot;,
  &amp;quot;Year&amp;quot;: 1963,
  &amp;quot;Years since joining&amp;quot;: 52,
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the API sits behind Cloudflare with a year-long cache expiry header, responses to any query like this should be lightning-fast.&lt;/p&gt;
&lt;p&gt;Datasette supports a limited form of filtering based on URL parameters, inspired by Django’s ORM. Here’s an example: by appending &lt;code&gt;?CLOUDS=1&amp;amp;MOUNTAINS=1&amp;amp;BUSHES=1&lt;/code&gt; to the FiveThirtyEight dataset of episodes of &lt;a href="https://en.wikipedia.org/wiki/The_Joy_of_Painting"&gt;Bob Ross’ The Joy of Painting&lt;/a&gt; we can see every episode in which Bob paints clouds, bushes AND mountains:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://fivethirtyeight.datasettes.com/fivethirtyeight-2628db9/bob-ross%2Felements-by-episode?CLOUDS=1&amp;amp;MOUNTAINS=1&amp;amp;BUSHES=1"&gt;https://fivethirtyeight.datasettes.com/fivethirtyeight-2628db9/bob-ross%2Felements-by-episode?CLOUDS=1&amp;amp;MOUNTAINS=1&amp;amp;BUSHES=1&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And here’s &lt;a href="https://fivethirtyeight.datasettes.com/fivethirtyeight-2628db9/bob-ross%2Felements-by-episode.jsono?CLOUDS=1&amp;amp;MOUNTAINS=1&amp;amp;BUSHES=1"&gt;the same episode list as JSON&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;&lt;a id="Arbitrary_SQL_55"&gt;&lt;/a&gt;Arbitrary SQL&lt;/h2&gt;
&lt;p&gt;The most exciting feature of datasette is that it allows users to execute &lt;em&gt;arbitrary SQL queries&lt;/em&gt; against the database. Here’s &lt;a href="https://fivethirtyeight.datasettes.com/fivethirtyeight?sql=select+%28select+sum%28%22APPLE_FRAME%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+APPLE_FRAME%2C%0D%0A%28select+sum%28%22AURORA_BOREALIS%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+AURORA_BOREALIS%2C%0D%0A%28select+sum%28%22BARN%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+BARN%2C%0D%0A%28select+sum%28%22BEACH%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+BEACH%2C%0D%0A%28select+sum%28%22BOAT%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+BOAT%2C%0D%0A%28select+sum%28%22BRIDGE%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+BRIDGE%2C%0D%0A%28select+sum%28%22BUILDING%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+BUILDING%2C%0D%0A%28select+sum%28%22BUSHES%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+BUSHES%2C%0D%0A%28select+sum%28%22CABIN%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+CABIN%2C%0D%0A%28select+sum%28%22CACTUS%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+CACTUS%2C%0D%0A%28select+sum%28%22CIRCLE_FRAME%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+CIRCLE_FRAME%2C%0D%0A%28select+sum%28%22CIRRUS%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+CIRRUS%2C%0D%0A%28select+sum%28%22CLIFF%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+CLIFF%2C%0D%0A%28select+sum%28%22CLOUDS%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+CLOUDS%2C%0D%0A%28select+sum%28%22CONIFER%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+CONIFER%2C%0D%0A%28select+sum%28%22CUMULUS%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+CUMULUS%2C%0D%0A%28select+sum%28%22DECIDUOUS%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+DECIDUOUS%2C%0D%0A%28select+sum%28%22DIANE_ANDRE%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+DIANE_ANDRE%2C%0D%0A%28select+sum%28%22DOCK%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+DOCK%2C%0D%0A%28select+sum%28%22DOUBLE_OVAL_FRAME%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+DOUBLE_OVAL_FRAME%2C%0D%0A%28select+sum%28%22FARM%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+FARM%2C%0D%0A%28select+sum%28%22FENCE%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+FENCE%2C%0D%0A%28select+sum%28%22FIRE%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+FIRE%2C%0D%0A%28select+sum%28%22FLORIDA_FRAME%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+FLORIDA_FRAME%2C%0D%0A%28select+sum%28%22FLOWERS%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+FLOWERS%2C%0D%0A%28select+sum%28%22FOG%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+FOG%2C%0D%0A%28select+sum%28%22FRAMED%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+FRAMED%2C%0D%0A%28select+sum%28%22GRASS%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+GRASS%2C%0D%0A%28select+sum%28%22GUEST%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+GUEST%2C%0D%0A%28select+sum%28%22HALF_CIRCLE_FRAME%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+HALF_CIRCLE_FRAME%2C%0D%0A%28select+sum%28%22HALF_OVAL_FRAME%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+HALF_OVAL_FRAME%2C%0D%0A%28select+sum%28%22HILLS%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+HILLS%2C%0D%0A%28select+sum%28%22LAKE%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+LAKE%2C%0D%0A%28select+sum%28%22LAKES%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+LAKES%2C%0D%0A%28select+sum%28%22LIGHTHOUSE%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+LIGHTHOUSE%2C%0D%0A%28select+sum%28%22MILL%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+MILL%2C%0D%0A%28select+sum%28%22MOON%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+MOON%2C%0D%0A%28select+sum%28%22MOUNTAIN%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+MOUNTAIN%2C%0D%0A%28select+sum%28%22MOUNTAINS%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+MOUNTAINS%2C%0D%0A%28select+sum%28%22NIGHT%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+NIGHT%2C%0D%0A%28select+sum%28%22OCEAN%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+OCEAN%2C%0D%0A%28select+sum%28%22OVAL_FRAME%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+OVAL_FRAME%2C%0D%0A%28select+sum%28%22PALM_TREES%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+PALM_TREES%2C%0D%0A%28select+sum%28%22PATH%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+PATH%2C%0D%0A%28select+sum%28%22PERSON%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+PERSON%2C%0D%0A%28select+sum%28%22PORTRAIT%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+PORTRAIT%2C%0D%0A%28select+sum%28%22RECTANGLE_3D_FRAME%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+RECTANGLE_3D_FRAME%2C%0D%0A%28select+sum%28%22RECTANGULAR_FRAME%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+RECTANGULAR_FRAME%2C%0D%0A%28select+sum%28%22RIVER%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+RIVER%2C%0D%0A%28select+sum%28%22ROCKS%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+ROCKS%2C%0D%0A%28select+sum%28%22SEASHELL_FRAME%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+SEASHELL_FRAME%2C%0D%0A%28select+sum%28%22SNOW%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+SNOW%2C%0D%0A%28select+sum%28%22SNOWY_MOUNTAIN%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+SNOWY_MOUNTAIN%2C%0D%0A%28select+sum%28%22SPLIT_FRAME%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+SPLIT_FRAME%2C%0D%0A%28select+sum%28%22STEVE_ROSS%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+STEVE_ROSS%2C%0D%0A%28select+sum%28%22STRUCTURE%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+STRUCTURE%2C%0D%0A%28select+sum%28%22SUN%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+SUN%2C%0D%0A%28select+sum%28%22TOMB_FRAME%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+TOMB_FRAME%2C%0D%0A%28select+sum%28%22TREE%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+TREE%2C%0D%0A%28select+sum%28%22TREES%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+TREES%2C%0D%0A%28select+sum%28%22TRIPLE_FRAME%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+TRIPLE_FRAME%2C%0D%0A%28select+sum%28%22WATERFALL%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+WATERFALL%2C%0D%0A%28select+sum%28%22WAVES%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+WAVES%2C%0D%0A%28select+sum%28%22WINDMILL%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+WINDMILL%2C%0D%0A%28select+sum%28%22WINDOW_FRAME%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+WINDOW_FRAME%2C%0D%0A%28select+sum%28%22WINTER%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+WINTER%2C%0D%0A%28select+sum%28%22WOOD_FRAMED%22%29+from+%5Bbob-ross%2Felements-by-episode%5D%29+as+WOOD_FRAMED%3B"&gt;a convoluted Bob Ross example&lt;/a&gt;, returning a count for each of the items that can appear in a painting.&lt;/p&gt;
&lt;p&gt;Datasette has a number of limitations in place here: it cuts off any SQL queries that take longer than a threshold (defaulting to 1000ms) and it refuses to return more than 1,000 rows at a time - partly to avoid too much JSON serialization overhead.&lt;/p&gt;
&lt;p&gt;Datasette also blocks queries containing the string &lt;code&gt;PRAGMA&lt;/code&gt;, since these statements &lt;a href="https://sqlite.org/pragma.html"&gt;could be used to modify database settings at runtime&lt;/a&gt;. If you need to include &lt;code&gt;PRAGMA&lt;/code&gt; in an argument to a query you can do so by constructing a prepared statement:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select * from [twitter-ratio/senators] where &amp;quot;text&amp;quot; like :q
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can then construct a URL that incorporates both the SQL and provides a value for that named argument, like this: &lt;a href="https://fivethirtyeight.datasettes.com/fivethirtyeight-2628db9?sql=select+rowid%2C+*+from+%5Btwitter-ratio%2Fsenators%5D+where+%22text%22+like+%3Aq&amp;amp;q=%25pragmatic%25"&gt;https://fivethirtyeight.datasettes.com/fivethirtyeight-2628db9?sql=select+rowid%2C+*+from+[twitter-ratio%2Fsenators]+where+“text”+like+%3Aq&amp;amp;q=%25pragmatic%25&lt;/a&gt; - which returns tweets by US senators that include the word “pragmatic”.&lt;/p&gt;
&lt;h2&gt;&lt;a id="Why_an_immutable_API_67"&gt;&lt;/a&gt;Why an immutable API?&lt;/h2&gt;
&lt;p&gt;A key feature of datasette is that the API it provides is very deliberately read-only. This provides a number of interesting benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It lets us use SQLite in production in high traffic scenarios. SQLite is an incredible piece of technology, but it is rarely used in web application contexts due to its limitations with respect to concurrent writes. Datasette opens SQLite files &lt;a href="https://sqlite.org/c3ref/open.html"&gt;using the immutable option&lt;/a&gt;, eliminating any concurrency concerns and allowing SQLite to go even faster for reads.&lt;/li&gt;
&lt;li&gt;Since the database is read-only, we can accept arbitrary SQL queries from our users!&lt;/li&gt;
&lt;li&gt;The datasette API bakes the first few characters of the sha256 hash of the database file contents into the API URLs themselves - for example in &lt;a href="https://parlgov.datasettes.com/parlgov-25f9855/cabinet"&gt;https://parlgov.datasettes.com/parlgov-25f9855/cabinet&lt;/a&gt;. This lets us serve year-long HTTP cache expiry headers, safe in the knowledge that any changes to the data will result in a change to the URL. These cache headers cause the content to be cached by both browsers and intermediary caches, such as Cloudflare.&lt;/li&gt;
&lt;li&gt;Read-only data makes datasette an ideal candidate for containerization. Deployments to Zeit Now happen using a Docker container, and the &lt;code&gt;datasette package&lt;/code&gt; command can be used to build a Docker image that bundles the database files and the datasette application together. If you need to scale to handle vast amounts of traffic, just deploy a bunch of extra containers and load-balance between them.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a id="Implementation_notes_76"&gt;&lt;/a&gt;Implementation notes&lt;/h2&gt;
&lt;p&gt;Datasette is built on top of the &lt;a href="https://github.com/channelcat/sanic"&gt;Sanic&lt;/a&gt; asynchronous Python web framework (see &lt;a href="https://simonwillison.net/2017/Oct/14/async-python-sanic-now/"&gt;my previous notes&lt;/a&gt;), and makes extensive use of Python 3’s async/await statements. Since SQLite doesn’t yet have an async Python module all interactions with SQLite are handled inside a thread pool managed by a &lt;a href="https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor"&gt;concurrent.futures.ThreadPoolExecutor&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The CLI is implemented using the &lt;a href="http://click.pocoo.org/"&gt;Click framework&lt;/a&gt;. This is the first time I’ve used Click and it was an absolute joy to work with. I enjoyed it so much I turned one of my Jupyter notebooks into a Click script called &lt;a href="https://github.com/simonw/csvs-to-sqlite"&gt;csvs-to-sqlite&lt;/a&gt; and published it to PyPI.&lt;/p&gt;

&lt;p&gt;This post is &lt;a href="https://news.ycombinator.com/item?id=15691409"&gt;being discussed on a Hacker News&lt;/a&gt;.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/apis"&gt;apis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cli"&gt;cli&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webapis"&gt;webapis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sanic"&gt;sanic&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="apis"/><category term="cli"/><category term="json"/><category term="projects"/><category term="sqlite"/><category term="webapis"/><category term="sanic"/><category term="docker"/><category term="datasette"/></entry><entry><title>hupper</title><link href="https://simonwillison.net/2017/Oct/23/hupper/#atom-tag" rel="alternate"/><published>2017-10-23T00:34:47+00:00</published><updated>2017-10-23T00:34:47+00:00</updated><id>https://simonwillison.net/2017/Oct/23/hupper/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://pypi.python.org/pypi/hupper"&gt;hupper&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Handy Python module for adding “live reload” development support to just about anything. I’m using it with Sanic—I run “hupper -m app” and it starts up my code in app.py and automatically reloads it any time any of the corresponding files changes on disk.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://github.com/channelcat/sanic/issues/168"&gt;Live reload server in develop phase · Issue #168 · channelcat/sanic · GitHub&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &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;/p&gt;



</summary><category term="python"/><category term="sanic"/></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><entry><title>Sanic</title><link href="https://simonwillison.net/2017/Oct/7/sanic/#atom-tag" rel="alternate"/><published>2017-10-07T18:39:05+00:00</published><updated>2017-10-07T18:39:05+00:00</updated><id>https://simonwillison.net/2017/Oct/7/sanic/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/channelcat/sanic"&gt;Sanic&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
“Sanic is a Flask-like Python 3.5+ web server that’s written to go fast [...] On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy”.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&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;/p&gt;



</summary><category term="async"/><category term="python"/><category term="sanic"/></entry></feed>