<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: api</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/api.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2024-12-17T23:50:12+00:00</updated><author><name>Simon Willison</name></author><entry><title>OpenAI WebRTC Audio demo</title><link href="https://simonwillison.net/2024/Dec/17/openai-webrtc/#atom-tag" rel="alternate"/><published>2024-12-17T23:50:12+00:00</published><updated>2024-12-17T23:50:12+00:00</updated><id>https://simonwillison.net/2024/Dec/17/openai-webrtc/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/openai-webrtc"&gt;OpenAI WebRTC Audio demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
OpenAI announced &lt;a href="https://openai.com/index/o1-and-new-tools-for-developers/"&gt;a bunch of API features&lt;/a&gt; today, including a brand new &lt;a href="https://platform.openai.com/docs/guides/realtime-webrtc"&gt;WebRTC API&lt;/a&gt; for setting up a two-way audio conversation with their models.&lt;/p&gt;
&lt;p&gt;They &lt;a href="https://twitter.com/OpenAIDevs/status/1869116585044259059"&gt;tweeted this opaque code example&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;async function createRealtimeSession(inStream, outEl, token) {
const pc = new RTCPeerConnection();
pc.ontrack = e =&amp;gt; outEl.srcObject = e.streams[0];
pc.addTrack(inStream.getTracks()[0]);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const headers = { Authorization: &lt;code&gt;Bearer ${token}&lt;/code&gt;, 'Content-Type': 'application/sdp' };
const opts = { method: 'POST', body: offer.sdp, headers };
const resp = await fetch('https://api.openai.com/v1/realtime', opts);
await pc.setRemoteDescription({ type: 'answer', sdp: await resp.text() });
return pc;
}&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So I &lt;a href="https://gist.github.com/simonw/69151091f7672adb9b42f5b17bd45d44"&gt;pasted that into Claude&lt;/a&gt; and had it build me &lt;a href="https://tools.simonwillison.net/openai-webrtc"&gt;this interactive demo&lt;/a&gt; for trying out the new API.&lt;/p&gt;
&lt;div style="max-width: 100%; margin: 1em 0"&gt;
    &lt;video 
        controls 
        preload="none"
        poster="https://static.simonwillison.net/static/2024/webrtc-demo.jpg" loop
        style="width: 100%; height: auto;"&gt;
        &lt;source src="https://static.simonwillison.net/static/2024/webrtc-demo.mp4" type="video/mp4"&gt;
    &lt;/video&gt;
&lt;/div&gt;

&lt;p&gt;My demo uses an OpenAI key directly, but the most interesting aspect of the new WebRTC mechanism is its support for &lt;a href="https://platform.openai.com/docs/guides/realtime-webrtc#creating-an-ephemeral-token"&gt;ephemeral tokens&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This solves a major problem with their previous realtime API: in order to connect to their endpoint you need to provide an API key, but that meant making that key visible to anyone who uses your application. The only secure way to handle this was to roll a full server-side proxy for their WebSocket API, just so you could hide your API key in your own server. &lt;a href="https://github.com/cloudflare/openai-workers-relay"&gt;cloudflare/openai-workers-relay&lt;/a&gt; is an example implementation of that pattern.&lt;/p&gt;
&lt;p&gt;Ephemeral tokens solve that by letting you make a server-side call to request an ephemeral token which will only allow a connection to be initiated to their WebRTC endpoint for the next 60 seconds. The user's browser then starts the connection, which will last for up to 30 minutes.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/audio"&gt;audio&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/security"&gt;security&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tools"&gt;tools&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cloudflare"&gt;cloudflare&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openai"&gt;openai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/multi-modal-output"&gt;multi-modal-output&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="audio"/><category term="security"/><category term="tools"/><category term="ai"/><category term="cloudflare"/><category term="openai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="claude"/><category term="multi-modal-output"/></entry><entry><title>Datasette 1.0a2: Upserts and finely grained permissions</title><link href="https://simonwillison.net/2022/Dec/15/datasette-1a2/#atom-tag" rel="alternate"/><published>2022-12-15T17:58:19+00:00</published><updated>2022-12-15T17:58:19+00:00</updated><id>https://simonwillison.net/2022/Dec/15/datasette-1a2/#atom-tag</id><summary type="html">
    &lt;p&gt;I've released the third alpha of Datasette 1.0. The &lt;a href="https://docs.datasette.io/en/latest/changelog.html#a2-2022-12-14"&gt;1.0a2 release&lt;/a&gt; introduces upsert support to the new JSON API and makes some major improvements to the Datasette permissions system.&lt;/p&gt;
&lt;p&gt;Here are the annotated releases (&lt;a href="https://simonwillison.net/series/datasette-release-notes/"&gt;see previous&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;You can install and try out the alpha using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install datasette==1.0a2
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Upserts for the JSON API&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;New &lt;code&gt;/db/table/-/upsert&lt;/code&gt; API, &lt;a href="https://docs.datasette.io/en/latest/json_api.html#tableupsertview"&gt;documented here&lt;/a&gt;. Upsert is an update-or-insert: existing rows will have specified keys updated, but if no row matches the incoming primary key a brand new row will be inserted instead. (&lt;a href="https://github.com/simonw/datasette/issues/1878"&gt;#1878&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I &lt;a href="https://simonwillison.net/2022/Dec/2/datasette-write-api/"&gt;wrote about the new JSON Write API&lt;/a&gt; when I released the first alpha a couple of weeks ago.&lt;/p&gt;
&lt;p&gt;The API can be used to create and drop tables, and to insert, update and delete rows in those tables.&lt;/p&gt;
&lt;p&gt;The new &lt;code&gt;/db/table/-/upsert&lt;/code&gt; API adds &lt;a href="https://docs.datasette.io/en/latest/json_api.html#tableupsertview"&gt;upsert support&lt;/a&gt; to Datasette.&lt;/p&gt;
&lt;p&gt;An &lt;em&gt;upsert&lt;/em&gt; is a update-or-insert. Consider the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /books/authors/-/upsert
Authorization: Bearer $TOKEN
Content-Type: application/json
&lt;/code&gt;&lt;/pre&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;{
  &lt;span class="pl-ent"&gt;"rows"&lt;/span&gt;: [
    {
      &lt;span class="pl-ent"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;1&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Ursula K. Le Guin&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"born"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;1929-10-21&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },
    {
      &lt;span class="pl-ent"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;2&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Terry Pratchett&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"born"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;1948-04-28&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },
    {
      &lt;span class="pl-ent"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;3&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Neil Gaiman&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"born"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;1960-11-10&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    }
  ]
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This table has a primary key of &lt;code&gt;id&lt;/code&gt;. The above API call will create three records if the table is empty. But if the table already has records matching any of those primary keys, their &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;born&lt;/code&gt; columns will be updated to match the incoming data.&lt;/p&gt;
&lt;p&gt;Upserts can be a really convenient way of synchronizing data with an external data source. I had a couple of enquiries about them when I published the first alpha, so I decided to make them a key feature for this release.&lt;/p&gt;
&lt;h4&gt;Ignore and replace for the create table API&lt;/h4&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;/db/-/create&lt;/code&gt; API for &lt;a href="https://docs.datasette.io/en/latest/json_api.html#tablecreateview"&gt;creating a table&lt;/a&gt; now accepts &lt;code&gt;"ignore": true&lt;/code&gt; and &lt;code&gt;"replace": true&lt;/code&gt; options when called with the &lt;code&gt;"rows"&lt;/code&gt; property that creates a new table based on an example set of rows. This means the API can be called multiple times with different rows, setting rules for what should happen if a primary key collides with an existing row. (&lt;a href="https://github.com/simonw/datasette/issues/1927"&gt;#1927&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/db/-/create&lt;/code&gt; API now requires actor to have &lt;code&gt;insert-row&lt;/code&gt; permission in order to use the &lt;code&gt;"row"&lt;/code&gt; or &lt;code&gt;"rows"&lt;/code&gt; properties. (&lt;a href="https://github.com/simonw/datasette/issues/1937"&gt;#1937&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This feature is a little less obvious, but I think it's going to be really useful.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;/db/-/create&lt;/code&gt; API can be used to create a new table. You can feed it &lt;a href="https://docs.datasette.io/en/latest/json_api.html#creating-a-table"&gt;an explicit list of columns&lt;/a&gt;, but you can also give it one or more rows and have it &lt;a href="https://docs.datasette.io/en/latest/json_api.html#creating-a-table-from-example-data"&gt;infer the correct schema&lt;/a&gt; based on those examples.&lt;/p&gt;
&lt;p&gt;Datasette inherits this feature &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#inserting-json-data"&gt;from sqlite-utils&lt;/a&gt; - I've been finding this an incredibly productive way to work with SQLite databases for a few years now.&lt;/p&gt;
&lt;p&gt;The real magic of this feature is that you can pipe data into Datasette without even needing to first check that the appropriate table has been created. It's a really fast way of getting from data to a populated database and a working API.&lt;/p&gt;
&lt;p&gt;Prior to 1.0a2 you could call &lt;code&gt;/db/-/create&lt;/code&gt; with &lt;code&gt;"rows"&lt;/code&gt; more than once and it would &lt;em&gt;probably&lt;/em&gt; work... unless you attempted to insert rows with primary keys that were already in use - in which case you would get an error. This limited the utility of the feature.&lt;/p&gt;
&lt;p&gt;Now you can pass &lt;code&gt;"ignore": true&lt;/code&gt; or &lt;code&gt;"replace": true&lt;/code&gt; to the API call, to tell Datasette what to do if it encounters a primary key that already exists in the table.&lt;/p&gt;
&lt;p&gt;Heres an example using the author data from above:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /books/-/create
Authorization: Bearer $TOKEN
Content-Type: application/json
&lt;/code&gt;&lt;/pre&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;{
  &lt;span class="pl-ent"&gt;"table"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;authors&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"pk"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;id&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"replace"&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"rows"&lt;/span&gt;: [
    {
      &lt;span class="pl-ent"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;1&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Ursula K. Le Guin&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"born"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;1929-10-21&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },
    {
      &lt;span class="pl-ent"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;2&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Terry Pratchett&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"born"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;1948-04-28&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },
    {
      &lt;span class="pl-ent"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;3&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Neil Gaiman&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"born"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;1960-11-10&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    }
  ]
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This will create the &lt;code&gt;authors&lt;/code&gt; table if it does not exist and ensure that those three rows exist in it, in their exact state. If a row already exists it will be replaced.&lt;/p&gt;
&lt;p&gt;Note that this is subtly different from an upsert. An upsert will only update the columns that were provided in the incoming data, leaving any other columns unchanged. A replace will replace the entire row.&lt;/p&gt;
&lt;h4 id="finely-grained-permissions"&gt;Finely grained permissions&lt;/h4&gt;
&lt;p&gt;This is the most significant area of improvement in this release.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;New &lt;a href="https://docs.datasette.io/en/latest/plugin_hooks.html#plugin-register-permissions"&gt;register_permissions(datasette)&lt;/a&gt; plugin hook. Plugins can now register named permissions, which will then be listed in various interfaces that show available permissions. (&lt;a href="https://github.com/simonw/datasette/issues/1940"&gt;#1940&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Prior to this, permissions were just strings - things like &lt;code&gt;"view-instance"&lt;/code&gt; or &lt;code&gt;"view-table"&lt;/code&gt; or &lt;code&gt;"insert-row"&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Plugins can introduce their own permissions - many do already, like &lt;a href="https://datasette.io/plugins/datasette-edit-schema"&gt;datasette-edit-schema&lt;/a&gt; which adds a &lt;code&gt;"edit-schema"&lt;/code&gt; permission.&lt;/p&gt;
&lt;p&gt;In order to start building UIs for managing permissions, I needed Datasette to know what they were!&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://docs.datasette.io/en/latest/plugin_hooks.html#plugin-register-permissions"&gt;register_permissions() hook&lt;/a&gt; lets them do exactly that, and Datasette core uses it to register its own default set of permissions too.&lt;/p&gt;
&lt;p&gt;Permissions are registered using the following named tuple:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-v"&gt;Permission&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;collections&lt;/span&gt;.&lt;span class="pl-en"&gt;namedtuple&lt;/span&gt;(
    &lt;span class="pl-s"&gt;"Permission"&lt;/span&gt;, (
        &lt;span class="pl-s"&gt;"name"&lt;/span&gt;, &lt;span class="pl-s"&gt;"abbr"&lt;/span&gt;, &lt;span class="pl-s"&gt;"description"&lt;/span&gt;,
        &lt;span class="pl-s"&gt;"takes_database"&lt;/span&gt;, &lt;span class="pl-s"&gt;"takes_resource"&lt;/span&gt;, &lt;span class="pl-s"&gt;"default"&lt;/span&gt;
    )
)&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;abbr&lt;/code&gt; is an abbreviation - e.g. &lt;code&gt;insert-row&lt;/code&gt; can be abbreviated to &lt;code&gt;ir&lt;/code&gt;. This is useful for creating things like signed API tokens where space is at a premium.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;takes_database&lt;/code&gt; and &lt;code&gt;takes_resource&lt;/code&gt; are booleans that indicate whether the permission can optionally be applied to a specific database (e.g. &lt;code&gt;execute-sql&lt;/code&gt;) or to a "resource", which is the name I'm now using for something that could be a SQL table, a SQL view or a &lt;a href="https://docs.datasette.io/en/stable/sql_queries.html#canned-queries"&gt;canned query&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;insert-row&lt;/code&gt; permission for example can be granted to the whole of Datasette, or to all tables in a specific database, or to specific tables.&lt;/p&gt;
&lt;p&gt;Finally, the &lt;code&gt;default&lt;/code&gt; value is a boolean that indicates whether the permission should be default-allow (&lt;code&gt;view-instance&lt;/code&gt; for example) or default-deny (&lt;code&gt;create-table&lt;/code&gt; and suchlike).&lt;/p&gt;
&lt;p&gt;This next feature explains why I needed those permission names to be known to Datasette:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;/-/create-token&lt;/code&gt; page can now be used to create API tokens which are restricted to just a subset of actions, including against specific databases or resources. See &lt;a href="https://docs.datasette.io/en/latest/authentication.html#createtokenview"&gt;API Tokens&lt;/a&gt; for details. (&lt;a href="https://github.com/simonw/datasette/issues/1947"&gt;#1947&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Datasette now has finely grained permissions for API tokens!&lt;/p&gt;
&lt;p&gt;This is the feature I always want when I'm working with other APIs: the ability to create a token that can only perform a restricted subset of actions.&lt;/p&gt;
&lt;p&gt;When I'm working with the GitHub API for example I frequently find myself wanting to create a "personal access token" that only has the ability to read issues from a specific repository. It's infuriating how many APIs leave this ability out.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;/-/create-token&lt;/code&gt; interface (which you can try out on &lt;a href="https://latest.datasette.io/"&gt;latest.datasette.io&lt;/a&gt; by first &lt;a href="https://latest.datasette.io/login-as-root"&gt;signing in as root&lt;/a&gt; and then &lt;a href="https://latest.datasette.io/-/create-token"&gt;visiting this page&lt;/a&gt;) lets you create an API token that can act on your behalf... and then optionally specify a subset of actions that the token is allowed to perform.&lt;/p&gt;
&lt;p&gt;Thanks to the new permissions registration system, the UI on that page knows which permissions can be applied to which entities within Datasette itself.&lt;/p&gt;
&lt;p&gt;Here's a partial screenshot of the UI:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/create-api-token.jpg" alt="Create an API token. This token will allow API access with the same abilities as your current user, root. Form lets you select Expires after X hours, 5 - and there's an expanded menu item for Restrict actions that can be performed using this token. Below that are lists of checkboxes - the first is for All databases and tables, with a list of every permission known to Datasette. Next is All tables in fixtures, which lists just permissions that make sense for a specific database. Finally is Specific tables, which lists fixtures: primary_key_multiple_columns with a much shorter list of permissions that can apply just to tables." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Select a subset of permissions, hit "Create token" and the result will be an access token you can copy and paste into another application, or use to call Datasette with &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here's an example token I created that grants &lt;code&gt;view-instance&lt;/code&gt; permission against all of Datasette, and &lt;code&gt;view-database&lt;/code&gt; and &lt;code&gt;execute-sql&lt;/code&gt; permission against the &lt;code&gt;ephemeral&lt;/code&gt; database, which in that demo is hidden from anonymous Datasette users.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dstok_eyJhIjoicm9vdCIsInQiOjE2NzEwODUzMDIsIl9yIjp7ImEiOlsidmkiXSwiZCI6eyJlcGhlbWVyYWwiOlsidmQiLCJlcyJdfX19.1uw0xyx8UND_Y_vTVg5kEF6m3GU&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Here's a screenshot of the screen I saw when I created it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/token-created.jpg" alt="Create an API token. Your API token: a copy-to-clipboard box with a long token in it. The token details area is expanded to show JSON that represents the permissions granted with that token." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;I've expanded the "token details" section to show the JSON that is bundled inside the signed token. The &lt;code&gt;"_r"&lt;/code&gt; block records the specific permissions granted by the token:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"a": ["vi"]&lt;/code&gt; indicates that the &lt;code&gt;view-instance&lt;/code&gt; permission is granted against all of Datasette.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"d": {"ephemeral": ["vd", "es"]}&lt;/code&gt; indicates that the &lt;code&gt;view-database&lt;/code&gt; and &lt;code&gt;execute-sql&lt;/code&gt; permissions are granted against the &lt;code&gt;ephemeral&lt;/code&gt; database.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The token also contains the ID of the user who created it (&lt;code&gt;"a": "root"&lt;/code&gt;) and the time that the token was created (&lt;code&gt;"t": 1671085302&lt;/code&gt;). If the token was set to expire that expiry duration would be baked in here as well.&lt;/p&gt;
&lt;p&gt;You can see the effect this has on the command-line using &lt;code&gt;curl&lt;/code&gt; like so:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;curl 'https://latest.datasette.io/ephemeral.json?sql=select+3+*+5&amp;amp;_shape=array'&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This will return a forbidden error. But if you add the signed token:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;curl 'https://latest.datasette.io/ephemeral.json?sql=select+3+*+5&amp;amp;_shape=array' -H 'Authorization: Bearer dstok_eyJhIjoicm9vdCIsInQiOjE2NzEwODUzMDIsIl9yIjp7ImEiOlsidmkiXSwiZCI6eyJlcGhlbWVyYWwiOlsidmQiLCJlcyJdfX19.1uw0xyx8UND_Y_vTVg5kEF6m3GU'&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;You'll get back a JSON response:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;[{&lt;span class="pl-ent"&gt;"3 * 5"&lt;/span&gt;: &lt;span class="pl-c1"&gt;15&lt;/span&gt;}]&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;The datasette create-token CLI tool&lt;/h4&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Likewise, the &lt;code&gt;datasette create-token&lt;/code&gt; CLI command can now create tokens with &lt;a href="https://docs.datasette.io/en/latest/authentication.html#authentication-cli-create-token-restrict"&gt;a subset of permissions&lt;/a&gt;. (&lt;a href="https://github.com/simonw/datasette/issues/1855"&gt;#1855&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;New &lt;a href="https://docs.datasette.io/en/latest/internals.html#create-token-actor-id-expires-after-none-restrict-all-none-restrict-database-none-restrict-resource-none"&gt;datasette.create_token()&lt;/a&gt; API method for programmatically creating signed API tokens. (&lt;a href="https://github.com/simonw/datasette/issues/1951"&gt;#1951&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;The other way you can create Datasette tokens is on the command-line, using &lt;a href="https://docs.datasette.io/en/latest/authentication.html#datasette-create-token"&gt;the datasette create-token command&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That's been upgraded to support finely grained permissions too.&lt;/p&gt;
&lt;p&gt;Here's how you'd create a token for the same set of permissions as my ephemeral example above:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette create-token root \
  --all view-instance \
  --database ephemeral view-database \
  --database ephemeral execute-sql \
  --secret MY_DATASETTE_SECRET
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In order to sign the token you need to pass in the &lt;code&gt;--secret&lt;/code&gt; used by the server - although it will pick that up from the &lt;code&gt;DATASETTE_SECRET&lt;/code&gt; environment variable if it's available.&lt;/p&gt;
&lt;p&gt;This has the interesting side-effect that you can use that command to create valid tokens for other Datasette instances, provided you know the secret they're using. I think this ability will be really useful for people like myself who run lots of different Datasette instances on stateless hosting platforms such as Vercel and Google Cloud Run.&lt;/p&gt;
&lt;h4&gt;Configuring permissions in metadata.json/yaml&lt;/h4&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Arbitrary permissions can now be configured at the instance, database and resource (table, SQL view or canned query) level in Datasette's &lt;a href="https://docs.datasette.io/en/latest/metadata.html#metadata"&gt;Metadata&lt;/a&gt; JSON and YAML files. The new &lt;code&gt;"permissions"&lt;/code&gt; key can be used to specify which actors should have which permissions. See &lt;a href="https://docs.datasette.io/en/latest/authentication.html#authentication-permissions-other"&gt;Other permissions in metadata&lt;/a&gt; for details. (&lt;a href="https://github.com/simonw/datasette/issues/1636"&gt;#1636&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Datasette has long had the ability to set permissions for viewing databases and tables using blocks of configuration in the increasingly poorly named &lt;a href="https://docs.datasette.io/en/stable/metadata.html"&gt;metadata.json/yaml files&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As I've built new plugins that introduce new permissions, I've found myself wishing for an easier way to say "user X is allowed to perform action Y" for arbitrary other permissions.&lt;/p&gt;
&lt;p&gt;The new &lt;code&gt;"permissions"&lt;/code&gt; key in metadata.json/yaml files allows you to do that.&lt;/p&gt;
&lt;p&gt;Here's how to specify that the user with &lt;code&gt;"id": "simon"&lt;/code&gt; is allowed to use the API to create tables and insert data into the &lt;code&gt;docs&lt;/code&gt; database:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;&lt;span class="pl-ent"&gt;databases&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;docs&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;permissions&lt;/span&gt;:
      &lt;span class="pl-ent"&gt;create-table&lt;/span&gt;:
        &lt;span class="pl-ent"&gt;id&lt;/span&gt;: &lt;span class="pl-s"&gt;simon&lt;/span&gt;
      &lt;span class="pl-ent"&gt;insert-row&lt;/span&gt;:
        &lt;span class="pl-ent"&gt;id&lt;/span&gt;: &lt;span class="pl-s"&gt;simon&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here's a demo you can run on your own machine. Save the above to &lt;code&gt;permissions.yaml&lt;/code&gt; and run the following in one terminal window:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette docs.db --create --secret sekrit -m permissions.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will create the &lt;code&gt;docs.db&lt;/code&gt; database if it doesn't already exist, and start Datasette with the &lt;code&gt;permissions.yaml&lt;/code&gt; metadata file.&lt;/p&gt;
&lt;p&gt;It sets &lt;code&gt;--secret&lt;/code&gt; to a known value (you should always use a random secure secret in production) so we can easily use it with &lt;code&gt;create-token&lt;/code&gt; in the next step:&lt;/p&gt;
&lt;p&gt;Then in another terminal window run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export TOKEN=$(
  datasette create-token simon \
  --secret sekrit
)
curl -XPOST http://localhost:8001/docs/-/create \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "table": "demo",
    "row": {"id": 1, "name": "Simon"},
    "pk": "id"
  }'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first line creates a token that can act on behalf of the &lt;code&gt;simon&lt;/code&gt; actor. The second &lt;code&gt;curl&lt;/code&gt; line then uses that token to create a table using the &lt;code&gt;/-/create&lt;/code&gt; endpoint.&lt;/p&gt;
&lt;p&gt;Run this, then visit &lt;a href="http://localhost:8001/docs/demo"&gt;http://localhost:8001/docs/demo&lt;/a&gt; to see the newly created table.&lt;/p&gt;
&lt;h4&gt;What's next?&lt;/h4&gt;
&lt;p&gt;With the 1.0a2 release I'm reasonably confident that Datasette 1.0 is new-feature-complete. There's still a &lt;strong&gt;lot&lt;/strong&gt; of work to do before the final release, but the remaining work is far more intimidating: I need to make clean backwards-incompatible breakages to a whole host of existing features in order to ship a 1.0 that I can keep stable for as long as possible.&lt;/p&gt;
&lt;p&gt;First up: I'm going to &lt;a href="https://github.com/simonw/datasette/issues/1914"&gt;redesign Datasette's default API output&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The current default JSON output for a simple table looks like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;{
  &lt;span class="pl-ent"&gt;"database"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;fixtures&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"table"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;facet_cities&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"is_view"&lt;/span&gt;: &lt;span class="pl-c1"&gt;false&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"human_description_en"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;sorted by name&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"rows"&lt;/span&gt;: [
    [
      &lt;span class="pl-c1"&gt;3&lt;/span&gt;,
      &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Detroit&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    ],
    [
      &lt;span class="pl-c1"&gt;2&lt;/span&gt;,
      &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Los Angeles&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    ],
    [
      &lt;span class="pl-c1"&gt;4&lt;/span&gt;,
      &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Memnonia&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    ],
    [
      &lt;span class="pl-c1"&gt;1&lt;/span&gt;,
      &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;San Francisco&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    ]
  ],
  &lt;span class="pl-ent"&gt;"truncated"&lt;/span&gt;: &lt;span class="pl-c1"&gt;false&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"filtered_table_rows_count"&lt;/span&gt;: &lt;span class="pl-c1"&gt;4&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"expanded_columns"&lt;/span&gt;: [],
  &lt;span class="pl-ent"&gt;"expandable_columns"&lt;/span&gt;: [],
  &lt;span class="pl-ent"&gt;"columns"&lt;/span&gt;: [
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;id&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;name&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
  ],
  &lt;span class="pl-ent"&gt;"primary_keys"&lt;/span&gt;: [
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;id&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
  ],
  &lt;span class="pl-ent"&gt;"units"&lt;/span&gt;: {},
  &lt;span class="pl-ent"&gt;"query"&lt;/span&gt;: {
    &lt;span class="pl-ent"&gt;"sql"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;select id, name from facet_cities order by name limit 101&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-ent"&gt;"params"&lt;/span&gt;: {}
  },
  &lt;span class="pl-ent"&gt;"facet_results"&lt;/span&gt;: {},
  &lt;span class="pl-ent"&gt;"suggested_facets"&lt;/span&gt;: [],
  &lt;span class="pl-ent"&gt;"next"&lt;/span&gt;: &lt;span class="pl-c1"&gt;null&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"next_url"&lt;/span&gt;: &lt;span class="pl-c1"&gt;null&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"private"&lt;/span&gt;: &lt;span class="pl-c1"&gt;false&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"allow_execute_sql"&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"query_ms"&lt;/span&gt;: &lt;span class="pl-c1"&gt;6.718471999647591&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"source"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;tests/fixtures.py&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"source_url"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;https://github.com/simonw/datasette/blob/main/tests/fixtures.py&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"license"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Apache License 2.0&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"license_url"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;https://github.com/simonw/datasette/blob/main/LICENSE&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In addition to being &lt;em&gt;really&lt;/em&gt; verbose, you'll note that the rows themselves are represented like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;[
  [
    &lt;span class="pl-c1"&gt;3&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Detroit&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
  ],
  [
    &lt;span class="pl-c1"&gt;2&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Los Angeles&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
  ],
  [
    &lt;span class="pl-c1"&gt;4&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Memnonia&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
  ],
  [
    &lt;span class="pl-c1"&gt;1&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;San Francisco&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
  ]
]&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I originally designed it this way because I thought saving on repeating the column names for every row would be more efficient.&lt;/p&gt;
&lt;p&gt;In practice, every single time I've used Datasette's API I've found myself using the &lt;code&gt;?_shape=array&lt;/code&gt; parameter, which outputs this format instead:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;[
  {
    &lt;span class="pl-ent"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;3&lt;/span&gt;,
    &lt;span class="pl-ent"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Detroit&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
  },
  {
    &lt;span class="pl-ent"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;2&lt;/span&gt;,
    &lt;span class="pl-ent"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Los Angeles&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
  },
  {
    &lt;span class="pl-ent"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;4&lt;/span&gt;,
    &lt;span class="pl-ent"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Memnonia&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
  },
  {
    &lt;span class="pl-ent"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;1&lt;/span&gt;,
    &lt;span class="pl-ent"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;San Francisco&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
  }
]&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It's just so much more convenient to work with!&lt;/p&gt;
&lt;p&gt;So the new default format will look like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;{
  &lt;span class="pl-ent"&gt;"rows"&lt;/span&gt;: [
    {
      &lt;span class="pl-ent"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;3&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Detroit&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },
    {
      &lt;span class="pl-ent"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;2&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Los Angeles&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },
    {
      &lt;span class="pl-ent"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;4&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Memnonia&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },
    {
      &lt;span class="pl-ent"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;1&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;San Francisco&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    }
  ]
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;rows&lt;/code&gt; key is there so I can add extra keys to the output, based on additional &lt;code&gt;?_extra=&lt;/code&gt; request parameters. You'll be able to get back everything you can get in the current full-fat table API, but you'll have to ask for it.&lt;/p&gt;
&lt;p&gt;There are a ton of other changes I want to make to Datasette as a whole - things like renaming &lt;code&gt;metadata.yaml&lt;/code&gt; to &lt;code&gt;config.yaml&lt;/code&gt; to reflect that it's gone way beyond its origins as a way of attaching metadata to a database.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/simonw/datasette/milestone/7"&gt;1.0 milestone&lt;/a&gt; is a dumping ground for many of these ideas. It's not a canonical reference though: I'd be very surprised if everything currently in that milestone makes it into the final 1.0 release.&lt;/p&gt;
&lt;p&gt;As I get closer to 1.0 though I'll be refining that milestone so it should get more accurate over time.&lt;/p&gt;
&lt;p&gt;Once again: &lt;strong&gt;now is the time&lt;/strong&gt; to be providing feedback on this stuff! The &lt;a href="https://datasette.io/discord"&gt;Datasette Discord&lt;/a&gt; is a particularly valuable way for me to get feedback on the work so far, and my plans for the future.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/permissions"&gt;permissions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/upsert"&gt;upsert&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/annotated-release-notes"&gt;annotated-release-notes&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="api"/><category term="permissions"/><category term="projects"/><category term="upsert"/><category term="datasette"/><category term="annotated-release-notes"/></entry><entry><title>How do I receive automatic updates from a Facebook group by email?</title><link href="https://simonwillison.net/2017/Mar/5/how-do-i-receive-automatic/#atom-tag" rel="alternate"/><published>2017-03-05T08:11:00+00:00</published><updated>2017-03-05T08:11:00+00:00</updated><id>https://simonwillison.net/2017/Mar/5/how-do-i-receive-automatic/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;em&gt;My answer to &lt;a href="http://ask.metafilter.com/306584/How-do-I-receive-automatic-updates-from-a-Facebook-group-by-email#4437750"&gt;How do I receive automatic updates from a Facebook group by email?&lt;/a&gt; on Ask MetaFilter&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Facebook's API does provide a feed of recent posts to a group: &lt;a href="https://developers.facebook.com/docs/graph-api/reference/v2.8/group/feed"&gt;https://developers.facebook.com/docs/graph-api/reference/v2.8/group/feed&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It doesn't look like IFTTT knows how to pull that at the moment - you could send them a feature request. But there are tools out there that can convert a Facebook group feed into RSS which IFTTT should be able to consume. A quick search turned up this option: &lt;a href="https://www.wallflux.com"&gt;https://www.wallflux.com&lt;/a&gt;&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ask-metafilter"&gt;ask-metafilter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/facebook"&gt;facebook&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/apartmentsearch"&gt;apartmentsearch&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="api"/><category term="ask-metafilter"/><category term="facebook"/><category term="apartmentsearch"/></entry><entry><title>OpenPlatform Content API Explorer</title><link href="https://simonwillison.net/2010/May/20/explorer/#atom-tag" rel="alternate"/><published>2010-05-20T17:42:00+00:00</published><updated>2010-05-20T17:42:00+00:00</updated><id>https://simonwillison.net/2010/May/20/explorer/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://explorer.content.guardianapis.com/#/search?format=json"&gt;OpenPlatform Content API Explorer&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The new API explorer for the Guardian’s Content API.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/guardian"&gt;guardian&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openplatform"&gt;openplatform&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/recovered"&gt;recovered&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/api-explorer"&gt;api-explorer&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="guardian"/><category term="openplatform"/><category term="recovered"/><category term="api-explorer"/></entry><entry><title>The Guardian's Open Platform is open for business</title><link href="https://simonwillison.net/2010/May/20/openplatform/#atom-tag" rel="alternate"/><published>2010-05-20T17:40:00+00:00</published><updated>2010-05-20T17:40:00+00:00</updated><id>https://simonwillison.net/2010/May/20/openplatform/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.guardian.co.uk/open-platform/blog/open-for-business"&gt;The Guardian&amp;#x27;s Open Platform is open for business&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The Guardian’s Content API is now out of beta. Of particular interest: you can access basic article metadata (headline, URL and tags) without using an API key at all, and the API supports JSONP—just request format=json and include a callback=foo argument.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/guardian"&gt;guardian&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jsonp"&gt;jsonp&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openplatform"&gt;openplatform&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/recovered"&gt;recovered&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/content"&gt;content&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="guardian"/><category term="json"/><category term="jsonp"/><category term="openplatform"/><category term="recovered"/><category term="content"/></entry><entry><title>WildlifeNearYou talk at £5 app, and being Wired (not Tired)</title><link href="https://simonwillison.net/2010/Apr/11/wired/#atom-tag" rel="alternate"/><published>2010-04-11T20:42:11+00:00</published><updated>2010-04-11T20:42:11+00:00</updated><id>https://simonwillison.net/2010/Apr/11/wired/#atom-tag</id><summary type="html">
    &lt;p&gt;Two quick updates about &lt;a href="http://www.wildlifenearyou.com/"&gt;WildlifeNearYou&lt;/a&gt;. First up, I gave a talk about the site at &lt;a href="http://fivepoundapp.com/"&gt;£5 app&lt;/a&gt;, my favourite Brighton evening event which celebrates side projects and the joy of Making Stuff. I talked about the site's &lt;a href="http://simonwillison.net/2010/Jan/12/wildlifenearyou/"&gt;genesis on a fort&lt;/a&gt;, &lt;a href="http://www.wildlifenearyou.com/best/"&gt;crowdsourcing photo ratings&lt;/a&gt;, how we use &lt;a href="http://www.freebase.com/"&gt;Freebase&lt;/a&gt; and &lt;a href="http://dbpedia.org/"&gt;DBpedia&lt;/a&gt; and how integrating with Flickr's machine tags gave us &lt;a href="http://code.flickr.com/blog/2010/02/10/5-questions-for-simon-willison/"&gt;a powerful location API for free&lt;/a&gt;. Here's the video of the talk, courtesy of &lt;a href="http://ianozsvald.com/2010/03/31/22nd-5-app-write-up-for-wildlife-plaques-robots-go-and-golf-gadgets/" title="22nd £5 App Write-up for WildLife, Plaques, Robots, Go and Golf Gadgets"&gt;Ian Oszvald&lt;/a&gt;:&lt;/p&gt;

&lt;object width="450" height="255"&gt;&lt;param name="allowfullscreen" value="true" /&gt;&lt;param name="allowscriptaccess" value="always" /&gt;&lt;param name="movie" value="http://vimeo.com/moogaloop.swf?clip_id=10578232&amp;amp;server=vimeo.com&amp;amp;show_title=1&amp;amp;show_byline=1&amp;amp;show_portrait=0&amp;amp;color=00ADEF&amp;amp;fullscreen=1" /&gt;&lt;embed src="http://vimeo.com/moogaloop.swf?clip_id=10578232&amp;amp;server=vimeo.com&amp;amp;show_title=1&amp;amp;show_byline=1&amp;amp;show_portrait=0&amp;amp;color=00ADEF&amp;amp;fullscreen=1" type="application/x-shockwave-flash" allowfullscreen="true" allowscriptaccess="always" width="450" height="255"&gt;&lt;/embed&gt;&lt;/object&gt;&lt;p&gt;&lt;a href="http://vimeo.com/10578232"&gt;£5 App #22 WildLifeNearYou by Simon Willison and Natalie Downe&lt;/a&gt; from &lt;a href="http://vimeo.com/user707645"&gt;IanProCastsCoUk&lt;/a&gt; on &lt;a href="http://vimeo.com"&gt;Vimeo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Secondly, I'm excited to note that WildlifeNearYou spin-off &lt;a href="http://owlsnearyou.com/"&gt;OwlsNearYou.com&lt;/a&gt; is featured in UK Wired magazine's Wired / Tired / Expired column... and we're Wired!&lt;/p&gt;

&lt;p&gt;&lt;a href="http://www.flickr.com/photos/simon/4511451405/"&gt;&lt;img src="http://simonwillison.net/static/2010/wired-owls-small.jpg" alt="Wired / Tired / Expired column from May 2010 Wired UK" width="450" height="176" /&gt;&lt;/a&gt;&lt;/p&gt;

    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/crowdsourcing"&gt;crowdsourcing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fivepoundapp"&gt;fivepoundapp&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/flickr"&gt;flickr&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/freebase"&gt;freebase&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/owlsnearyou"&gt;owlsnearyou&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/my-talks"&gt;my-talks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/wildlifenearyou"&gt;wildlifenearyou&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/wired"&gt;wired&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="api"/><category term="crowdsourcing"/><category term="fivepoundapp"/><category term="flickr"/><category term="freebase"/><category term="natalie-downe"/><category term="owlsnearyou"/><category term="my-talks"/><category term="wildlifenearyou"/><category term="wired"/></entry><entry><title>jsondns</title><link href="https://simonwillison.net/2009/Dec/30/jsondns/#atom-tag" rel="alternate"/><published>2009-12-30T17:37:16+00:00</published><updated>2009-12-30T17:37:16+00:00</updated><id>https://simonwillison.net/2009/Dec/30/jsondns/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://dig.jsondns.org/"&gt;jsondns&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A JSONP API for making DNS queries, with a nice URL structure.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dns"&gt;dns&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jsonp"&gt;jsonp&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="dns"/><category term="json"/><category term="jsonp"/></entry><entry><title>Djangopeople JSON parser</title><link href="https://simonwillison.net/2009/Nov/28/djangopeople/#atom-tag" rel="alternate"/><published>2009-11-28T11:29:25+00:00</published><updated>2009-11-28T11:29:25+00:00</updated><id>https://simonwillison.net/2009/Nov/28/djangopeople/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.agmweb.ca/blog/andy/2233/"&gt;Djangopeople JSON parser&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Awesome—Andy McKay has compensated for the lack of an official DjangoPeople API by creating a JSONP screen scraped API and hosting it on App Engine. As far as I’m concerned this is an officially supported feature—I’ll make sure future site changes don’t break it, and when I do add an API I’ll try to keep it compatible and help Andy set up redirects.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/andy-mckay"&gt;andy-mckay&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django-people"&gt;django-people&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google-app-engine"&gt;google-app-engine&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jsonp"&gt;jsonp&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="andy-mckay"/><category term="api"/><category term="django"/><category term="django-people"/><category term="google-app-engine"/><category term="json"/><category term="jsonp"/><category term="python"/></entry><entry><title>Announcing the New York Times Campaign Finance API</title><link href="https://simonwillison.net/2008/Oct/15/announcing/#atom-tag" rel="alternate"/><published>2008-10-15T14:05:18+00:00</published><updated>2008-10-15T14:05:18+00:00</updated><id>https://simonwillison.net/2008/Oct/15/announcing/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://open.blogs.nytimes.com/2008/10/14/announcing-the-new-york-times-campaign-finance-api/"&gt;Announcing the New York Times Campaign Finance API&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The New York Times have released their first data API, exposing campaign finance data from the Federal Election Commission.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://www.readwriteweb.com/archives/the_first_new_york_times_api_i.php"&gt;ReadWriteWeb&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/campaignfinance"&gt;campaignfinance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/new-york-times"&gt;new-york-times&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="campaignfinance"/><category term="new-york-times"/></entry><entry><title>Tweetersation</title><link href="https://simonwillison.net/2008/Oct/2/tweetersation/#atom-tag" rel="alternate"/><published>2008-10-02T17:08:19+00:00</published><updated>2008-10-02T17:08:19+00:00</updated><id>https://simonwillison.net/2008/Oct/2/tweetersation/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://tweetersation.com/"&gt;Tweetersation&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Nat and my latest side project: a JSONP API powered tool to more easily follow conversations between people on Twitter, by combining their tweets in to a single timeline.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jsonp"&gt;jsonp&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/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tweetersation"&gt;tweetersation&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/twitter"&gt;twitter&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="javascript"/><category term="jsonp"/><category term="natalie-downe"/><category term="projects"/><category term="tweetersation"/><category term="twitter"/></entry><entry><title>IMG-2-JSON</title><link href="https://simonwillison.net/2008/Aug/12/imgjson/#atom-tag" rel="alternate"/><published>2008-08-12T09:43:00+00:00</published><updated>2008-08-12T09:43:00+00:00</updated><id>https://simonwillison.net/2008/Aug/12/imgjson/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://img2json.appspot.com/"&gt;IMG-2-JSON&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I’m not the only person deploying simple JSON-P APIs on App Engine: Adam Burmister’s tool extracts dimension, mimetype and EXIF metadata when provided the URL to an image file.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://ajaxian.com/archives/img2json-get-your-image-metadata-via-app-engine"&gt;Ajaxian&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/adam-burmister"&gt;adam-burmister&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/exif"&gt;exif&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google-app-engine"&gt;google-app-engine&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/img2json"&gt;img2json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jsonp"&gt;jsonp&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mimetype"&gt;mimetype&lt;/a&gt;&lt;/p&gt;



</summary><category term="adam-burmister"/><category term="api"/><category term="exif"/><category term="google-app-engine"/><category term="img2json"/><category term="json"/><category term="jsonp"/><category term="mimetype"/></entry><entry><title>jsontime</title><link href="https://simonwillison.net/2008/Jun/21/jsontime/#atom-tag" rel="alternate"/><published>2008-06-21T19:07:10+00:00</published><updated>2008-06-21T19:07:10+00:00</updated><id>https://simonwillison.net/2008/Jun/21/jsontime/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://json-time.appspot.com/"&gt;jsontime&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Nat and I threw this together this morning—it runs on Google App Engine and exposes Python’s pytz timezone library over JSONP.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google-app-engine"&gt;google-app-engine&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jsontime"&gt;jsontime&lt;/a&gt;, &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/pytz"&gt;pytz&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="google-app-engine"/><category term="javascript"/><category term="json"/><category term="jsontime"/><category term="projects"/><category term="python"/><category term="pytz"/></entry><entry><title>jQuery style chaining with the Django ORM</title><link href="https://simonwillison.net/2008/May/1/orm/#atom-tag" rel="alternate"/><published>2008-05-01T12:31:17+00:00</published><updated>2008-05-01T12:31:17+00:00</updated><id>https://simonwillison.net/2008/May/1/orm/#atom-tag</id><summary type="html">
    &lt;p&gt;Django's ORM is, in my opinion, the unsung gem of the framework. For the subset of SQL that's used in most web applications it's very hard to beat. It's a beautiful piece of API design, and I tip my hat to the people who designed and built it.&lt;/p&gt;

&lt;h4&gt;Lazy evaluation&lt;/h4&gt;

&lt;p&gt;If you haven't spent much time with the ORM, two key features are lazy evaluation and chaining. Consider the following statement:&lt;/p&gt;

&lt;pre&gt;&lt;code class="python"&gt;entries = Entry.objects.all()&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Assuming you have created an Entry model of some sort, the above statement will create a Django QuerySet object representing all of the entries in the database. It will &lt;em&gt;not&lt;/em&gt; result in the execution of any SQL - QuerySets are lazily evaluated, and are only executed at the last possible moment. The most common situation in which SQL will be executed is when the object is used for iteration:&lt;/p&gt;

&lt;pre&gt;&lt;code class="python"&gt;for entry in entries:
    print entry.title&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This usually happens in a template:&lt;/p&gt;

&lt;pre&gt;&lt;code class="python"&gt;&amp;lt;ul&amp;gt;
{% for entry in entries %}
  &amp;lt;li&amp;gt;{{ entry.title }}&amp;lt;/li&amp;gt;
{% endfor %}
&amp;lt;/ul&amp;gt;&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Lazy evaluation works nicely with  &lt;a href="http://www.djangoproject.com/documentation/cache/#template-fragment-caching"&gt;template fragment caching&lt;/a&gt; - even if you pass a QuerySet to a template it won't be executed if the fragment it is used in can be served from the cache.&lt;/p&gt;

&lt;p&gt;You can modify QuerySets as many times as you like before they are executed:&lt;/p&gt;

&lt;pre&gt;&lt;code class="python"&gt;entries = Entry.objects.all()
today = datetime.date.today()
entries_this_year = entries.filter(
    posted__year = today.year
)
entries_last_year = entries.filter(
    posted__year = today.year - 1
)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Again, no SQL has been executed, but we now have two QuerySets which, when iterated, will produce the desired result.&lt;/p&gt;

&lt;h4&gt;Chaining&lt;/h4&gt;

&lt;p&gt;Chaining comes in when you want to apply multiple modifications to a QuerySet. Here are blog entries from 2006 that weren't posted in January:&lt;/p&gt;

&lt;pre&gt;&lt;code class="python"&gt;Entry.objects.filter(
    posted__year = 2006
).exclude(posted__month = 1)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And here's entries from that year posted to the category named "Personal", ordered by title:&lt;/p&gt;

&lt;pre&gt;&lt;code class="python"&gt;Entry.objects.filter(
    posted__year = 2006
).filter(
    category__name = "Personal"
).order_by('title')&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The above can also be expressed like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class="python"&gt;Entry.objects.filter(
    posted__year = 2006,
    category__name = "Personal"
).order_by('title')&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Chaining in jQuery&lt;/h4&gt;

&lt;p&gt;The parallels to &lt;a href="http://jquery.com/"&gt;jQuery&lt;/a&gt; are pretty clear. The jQuery API is built around chaining, and the jQuery &lt;a href="http://docs.jquery.com/Effects"&gt;animation library&lt;/a&gt; even uses a form of lazy evaluation to automatically queue up effects to run in sequence:&lt;/p&gt;

&lt;pre&gt;&lt;code class="javascript"&gt;jQuery('div#message').addClass(
	'borderfade'
).animate({
   'borderWidth': '+10px'
}, 1000).fadeOut();&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One of the neatest things about jQuery is the plugin model, which takes advantage of JavaScript's prototype inheritance and makes it trivially easy to add new chainable methods. If we wanted to package the above dumb effect up as a plugin, we could do so like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class="javascript"&gt;jQuery.fn.dumbBorderFade = function() {
    return this.addClass(
        'borderfade'
    ).animate({
       'borderWidth': '+10px'
    }, 1000).fadeOut();
};&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we can apply it to an element like so:&lt;/p&gt;

&lt;pre&gt;&lt;code class="javascript"&gt;jQuery('div#message').dumbBorderFade();&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Custom QuerySet methods in Django&lt;/h4&gt;

&lt;p&gt;Django supports adding custom methods for accessing the ORM through the ability to implement a custom &lt;a href="http://www.djangoproject.com/documentation/model-api/#managers"&gt;Manager&lt;/a&gt;. In the above examples, &lt;code class="python"&gt;Entry.objects&lt;/code&gt; is the Manager. The downside of this approach is that methods added to a manager can only be used at the beginning of the chain.&lt;/p&gt;

&lt;p&gt;Luckily, Managers also provide a hook for returning a custom QuerySet. This means we can create our own QuerySet subclass and add new methods to it, in a way that's reminiscent of jQuery:&lt;/p&gt;

&lt;pre&gt;&lt;code class="python"&gt;from django.db import models
from django.db.models.query import QuerySet
import datetime

class EntryQuerySet(QuerySet):
    def on_date(self, date):
        next = date + datetime.timedelta(days = 1)
        return self.filter(
            posted__gt = date,
            posted__lt = next
        )

class EntryManager(models.Manager):
    def get_query_set(self):
        return EntryQuerySet(self.model)

class Entry(models.Model):
    ...
    objects = EntryManager()&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The above gives us a new method on the QuerySets returned by Entry.objects called on_date(), which lets us filter entries down to those posted on a specific date. Now we can run queries like the following:&lt;/p&gt;

&lt;pre&gt;&lt;code class="python"&gt;Entry.objects.filter(
    category__name = 'Personal'
).on_date(datetime.date(2008, 5, 1))&lt;/code&gt;&lt;/pre&gt;

&lt;h4&gt;Reducing the boilerplate&lt;/h4&gt;

&lt;p&gt;This method works fine, but it requires quite a bit of boilerplate code - a QuerySet subclass and a Manager subclass plus the wiring to pull them all together. Wouldn't it be neat if you could declare the extra QuerySet methods inside the model definition itself?&lt;/p&gt;

&lt;p&gt;It turns out you can, and it's surprisingly easy. Here's the syntax I came up with:&lt;/p&gt;

&lt;pre&gt;&lt;code class="python"&gt;from django.db.models.query import QuerySet

class Entry(models.Model):
   ...
   objects = QuerySetManager()
   ...
   class QuerySet(QuerySet):
       def on_date(self, date):
           return self.filter(
               ...
           )&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here I've made the custom QuerySet class an inner class of the model definition. I've also replaced the default manager with a QuerySetManager. All this class does is return the QuerySet inner class for the current model from get_query_set. The implementation looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code class="python"&gt;class QuerySetManager(models.Manager):
    def get_query_set(self):
        return self.model.QuerySet(self.model)&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I'm pretty happy with this; it makes it trivial to add custom QuerySet methods and does so without any monkeypatching or deep reliance on Django ORM internals. I think the ease with which this can be achieved is a testament to the quality of the ORM API.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chaining"&gt;chaining&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jquery"&gt;jquery&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/orm"&gt;orm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/queryset"&gt;queryset&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="api"/><category term="chaining"/><category term="django"/><category term="jquery"/><category term="orm"/><category term="python"/><category term="queryset"/></entry><entry><title>MediaWiki API</title><link href="https://simonwillison.net/2008/Apr/26/mediawiki/#atom-tag" rel="alternate"/><published>2008-04-26T18:47:31+00:00</published><updated>2008-04-26T18:47:31+00:00</updated><id>https://simonwillison.net/2008/Apr/26/mediawiki/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://en.wikipedia.org/w/api.php"&gt;MediaWiki API&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Wikipedia’s best kept secret?


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mediawiki"&gt;mediawiki&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/wikipedia"&gt;wikipedia&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="mediawiki"/><category term="wikipedia"/></entry><entry><title>Welcome to Fire Eagle!</title><link href="https://simonwillison.net/2008/Mar/5/welcome/#atom-tag" rel="alternate"/><published>2008-03-05T19:05:47+00:00</published><updated>2008-03-05T19:05:47+00:00</updated><id>https://simonwillison.net/2008/Mar/5/welcome/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://fireeagle.yahoo.net/"&gt;Welcome to Fire Eagle!&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
It’s launched! A service and accompanying API for saving your physical location and selectively sharing it with applications that you trust.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fireeagle"&gt;fireeagle&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/location"&gt;location&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/yahoo"&gt;yahoo&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="fireeagle"/><category term="location"/><category term="yahoo"/></entry><entry><title>Flickr Place IDs</title><link href="https://simonwillison.net/2008/Jan/19/flickr/#atom-tag" rel="alternate"/><published>2008-01-19T07:34:38+00:00</published><updated>2008-01-19T07:34:38+00:00</updated><id>https://simonwillison.net/2008/Jan/19/flickr/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://laughingmeme.org/2008/01/18/flickr-place-ids/"&gt;Flickr Place IDs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;code&gt;flickr.places.find&lt;/code&gt;, &lt;code&gt;flickr.places.resolvePlaceURL&lt;/code&gt; and &lt;code&gt;flickr.places.resolvePlaceID&lt;/code&gt; combine to provide a really useful, lightweight not-quite-a-geocoder API. It's a shame you can't search for places by providing a latitude/longitude point yet.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/flickr"&gt;flickr&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/flickrplaces"&gt;flickrplaces&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geocoding"&gt;geocoding&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/kellan-elliott-mccrea"&gt;kellan-elliott-mccrea&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="flickr"/><category term="flickrplaces"/><category term="geocoding"/><category term="geospatial"/><category term="kellan-elliott-mccrea"/></entry><entry><title>Google Chart API Revisited</title><link href="https://simonwillison.net/2007/Dec/7/little/#atom-tag" rel="alternate"/><published>2007-12-07T23:59:53+00:00</published><updated>2007-12-07T23:59:53+00:00</updated><id>https://simonwillison.net/2007/Dec/7/little/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://gulopine.gamemusic.org/2007/12/google-chart-api-revisited.html"&gt;Google Chart API Revisited&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Marty does some more digging.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google-charts"&gt;google-charts&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/martyalchin"&gt;martyalchin&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="google-charts"/><category term="martyalchin"/></entry><entry><title>hasAccount</title><link href="https://simonwillison.net/2007/Sep/28/as/#atom-tag" rel="alternate"/><published>2007-09-28T09:10:56+00:00</published><updated>2007-09-28T09:10:56+00:00</updated><id>https://simonwillison.net/2007/Sep/28/as/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.kryogenix.org/days/2007/09/28/hasaccount"&gt;hasAccount&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Stuart proposes a light-weight API for letting any site know if a user has an account (and is signed in) on another service. I wouldn’t want to deploy this without being confident that my CSRF protection was in order.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/accounts"&gt;accounts&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/crossdomain"&gt;crossdomain&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csrf"&gt;csrf&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/stuart-langridge"&gt;stuart-langridge&lt;/a&gt;&lt;/p&gt;



</summary><category term="accounts"/><category term="api"/><category term="crossdomain"/><category term="csrf"/><category term="json"/><category term="stuart-langridge"/></entry><entry><title>Freebase developer documentation</title><link href="https://simonwillison.net/2007/Sep/3/introduction/#atom-tag" rel="alternate"/><published>2007-09-03T02:38:01+00:00</published><updated>2007-09-03T02:38:01+00:00</updated><id>https://simonwillison.net/2007/Sep/3/introduction/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.freebase.com/view/helptopic?id=%239202a8c04000641f800000000544e14d"&gt;Freebase developer documentation&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The JSON API and particularly the query language are fascinating.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/freebase"&gt;freebase&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="freebase"/><category term="json"/></entry><entry><title>Wesabi: Your bank has a REST API now</title><link href="https://simonwillison.net/2007/Jul/12/wheaties/#atom-tag" rel="alternate"/><published>2007-07-12T17:20:48+00:00</published><updated>2007-07-12T17:20:48+00:00</updated><id>https://simonwillison.net/2007/Jul/12/wheaties/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://blog.wesabe.com/index.php/2007/07/12/your-bank-has-a-rest-api-now-shhh-dont-tell-them/"&gt;Wesabi: Your bank has a REST API now&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Excellent—I’ve been saying for a while now that I’d really love to be able to program my bank account.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/banking"&gt;banking&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rest"&gt;rest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/wesabi"&gt;wesabi&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="banking"/><category term="rest"/><category term="wesabi"/></entry><entry><title>Washington Post and Facebook</title><link href="https://simonwillison.net/2007/Jun/19/post/#atom-tag" rel="alternate"/><published>2007-06-19T10:33:58+00:00</published><updated>2007-06-19T10:33:58+00:00</updated><id>https://simonwillison.net/2007/Jun/19/post/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.devurandom.org/weblog/2007/jun/06/washington_post_facebook_part_two/"&gt;Washington Post and Facebook&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Deryck Hodge on hacking against Facebook API using  Django.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/deryckhodge"&gt;deryckhodge&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/facebook"&gt;facebook&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/washington-post"&gt;washington-post&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="deryckhodge"/><category term="django"/><category term="facebook"/><category term="washington-post"/></entry><entry><title>The Zonetag API Goes Public</title><link href="https://simonwillison.net/2007/Jun/2/geohackers/#atom-tag" rel="alternate"/><published>2007-06-02T00:53:01+00:00</published><updated>2007-06-02T00:53:01+00:00</updated><id>https://simonwillison.net/2007/Jun/2/geohackers/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://developer.yahoo.net/blog/archives/2007/06/geohackers_the.html"&gt;The Zonetag API Goes Public&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Awesome new API from YRB—given a cell tower ID can provide both a location and a list of suggested tags, based on data collected by ZoneTag.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cellid"&gt;cellid&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/location"&gt;location&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tagging"&gt;tagging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/yahoo"&gt;yahoo&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ydn"&gt;ydn&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/yrb"&gt;yrb&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zonetag"&gt;zonetag&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="cellid"/><category term="location"/><category term="tagging"/><category term="yahoo"/><category term="ydn"/><category term="yrb"/><category term="zonetag"/></entry><entry><title>Spinn3r Launches Today</title><link href="https://simonwillison.net/2007/Mar/29/spinnr/#atom-tag" rel="alternate"/><published>2007-03-29T14:34:20+00:00</published><updated>2007-03-29T14:34:20+00:00</updated><id>https://simonwillison.net/2007/Mar/29/spinnr/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://blog.spinn3r.com/2007/03/spinn3r_launche.html"&gt;Spinn3r Launches Today&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
An API to the Tailrank blog index, so you can index the blogosphere without having to build your own spider / spam filter.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/spinn3r"&gt;spinn3r&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tailrank"&gt;tailrank&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="spinn3r"/><category term="tailrank"/></entry><entry><title>Introducing the Yahoo! Mail Web Service</title><link href="https://simonwillison.net/2007/Mar/29/introducing/#atom-tag" rel="alternate"/><published>2007-03-29T02:47:09+00:00</published><updated>2007-03-29T02:47:09+00:00</updated><id>https://simonwillison.net/2007/Mar/29/introducing/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://developer.yahoo.net/blog/archives/2007/03/mail.html"&gt;Introducing the Yahoo! Mail Web Service&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
101 pages of documentation—this thing is huge!


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/yahoo"&gt;yahoo&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/yahoo-mail"&gt;yahoo-mail&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ydn"&gt;ydn&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="yahoo"/><category term="yahoo-mail"/><category term="ydn"/></entry><entry><title>Highrise Forum: Using the undocumented API</title><link href="https://simonwillison.net/2007/Mar/19/using/#atom-tag" rel="alternate"/><published>2007-03-19T23:29:13+00:00</published><updated>2007-03-19T23:29:13+00:00</updated><id>https://simonwillison.net/2007/Mar/19/using/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://forum.highrisehq.com/forums/2/topics/7"&gt;Highrise Forum: Using the undocumented API&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Add .xml to the end of many URLs in Highrise to get an XML representation of that page.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/highrise"&gt;highrise&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rest"&gt;rest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/xml"&gt;xml&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="highrise"/><category term="rest"/><category term="xml"/></entry><entry><title>Flickr Machine Tags</title><link href="https://simonwillison.net/2007/Jan/24/flickr/#atom-tag" rel="alternate"/><published>2007-01-24T19:54:52+00:00</published><updated>2007-01-24T19:54:52+00:00</updated><id>https://simonwillison.net/2007/Jan/24/flickr/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.flickr.com/groups/api/discuss/72157594497877875/"&gt;Flickr Machine Tags&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A new feature for API developers that lets them stuff arbritrary namespaced key/value pairs in to tags and query them using the API. Even without range queries, this will enable a ton of exciting new third party developments.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/flickr"&gt;flickr&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/machinetags"&gt;machinetags&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tagging"&gt;tagging&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="flickr"/><category term="machinetags"/><category term="tagging"/></entry><entry><title>Guide to the Dabble DB Plugin API</title><link href="https://simonwillison.net/2007/Jan/9/dabble/#atom-tag" rel="alternate"/><published>2007-01-09T11:37:26+00:00</published><updated>2007-01-09T11:37:26+00:00</updated><id>https://simonwillison.net/2007/Jan/9/dabble/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://dabbledb.com/help/guides/pluginapi/"&gt;Guide to the Dabble DB Plugin API&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is really nice—Dabble POSTs your plugin script a bunch of CSV values, your script returns CSV for the derived fields. Doesn’t seem to state which flavour of CSV though.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://www.cincomsmalltalk.com/blog/blogView?showComments=true&amp;amp;entry=3345752150"&gt;James Robertson&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chad-fowler"&gt;chad-fowler&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/csv"&gt;csv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dabbledb"&gt;dabbledb&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="chad-fowler"/><category term="csv"/><category term="dabbledb"/><category term="plugins"/></entry><entry><title>Writing a Jokosher extension</title><link href="https://simonwillison.net/2007/Jan/7/extension/#atom-tag" rel="alternate"/><published>2007-01-07T22:25:54+00:00</published><updated>2007-01-07T22:25:54+00:00</updated><id>https://simonwillison.net/2007/Jan/7/extension/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.kryogenix.org/days/2006/12/30/writing-a-jokosher-extension-a-rambling-essay"&gt;Writing a Jokosher extension&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I like the way API calls are made through an API object passed to the extension’s startup function.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jokosher"&gt;jokosher&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/stuart-langridge"&gt;stuart-langridge&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="jokosher"/><category term="python"/><category term="stuart-langridge"/></entry><entry><title>Abusing Amazon images</title><link href="https://simonwillison.net/2006/Dec/14/amazon/#atom-tag" rel="alternate"/><published>2006-12-14T19:38:34+00:00</published><updated>2006-12-14T19:38:34+00:00</updated><id>https://simonwillison.net/2006/Dec/14/amazon/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://aaugh.com/imageabuse.html"&gt;Abusing Amazon images&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Amazon have an amazingly flexible API for generating and modifying product images.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://xach.livejournal.com/95656.html"&gt;Zach&amp;#x27;s Journal&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/amazon"&gt;amazon&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;&lt;/p&gt;



</summary><category term="amazon"/><category term="api"/></entry><entry><title>Google's own cornershop</title><link href="https://simonwillison.net/2006/Dec/14/corners/#atom-tag" rel="alternate"/><published>2006-12-14T19:34:41+00:00</published><updated>2006-12-14T19:34:41+00:00</updated><id>https://simonwillison.net/2006/Dec/14/corners/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://xach.livejournal.com/95656.html"&gt;Google&amp;#x27;s own cornershop&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Google groups has an undocumented API for generating rounded corners.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api"&gt;api&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google"&gt;google&lt;/a&gt;&lt;/p&gt;



</summary><category term="api"/><category term="google"/></entry></feed>