<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: glitch</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/glitch.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2025-05-29T20:36:06+00:00</updated><author><name>Simon Willison</name></author><entry><title>Saying Bye to Glitch</title><link href="https://simonwillison.net/2025/May/29/saying-bye-to-glitch/#atom-tag" rel="alternate"/><published>2025-05-29T20:36:06+00:00</published><updated>2025-05-29T20:36:06+00:00</updated><id>https://simonwillison.net/2025/May/29/saying-bye-to-glitch/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://pketh.org/bye-glitch.html"&gt;Saying Bye to Glitch&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Pirijan, co-creator of &lt;a href="https://www.glitch.com/"&gt;Glitch&lt;/a&gt; - who stopped working on it six years ago, so has the benefit of distance:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Here lies Glitch, a place on the web you could go to write up a website or a node.js server that would be hosted and updated as you type. 🥀 RIP 2015 – 2025.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Pirijan continues with a poignant retrospective about Glitch's early origins at Fog Greek with the vision of providing "web development with real code that was as easy as editing a Google Doc". Their conclusion:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I still believe there’s a market for easy and fun web development and hosting, but a product like this needs power-users and enthusiasts willing to pay for it. To build any kind of prosumer software, you do have to be an optimist and believe that enough of the world still cares about quality and craft.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Glitch will be &lt;a href="https://blog.glitch.com/post/changes-are-coming-to-glitch/"&gt;shutting down project hosting and user profiles&lt;/a&gt; on July 8th.&lt;/p&gt;
&lt;p&gt;Code will be available to download until the end of the year. Glitch have &lt;a href="https://support.glitch.com/t/glitch-project-bulk-downloading/75872"&gt;an official Python export script&lt;/a&gt; that can download all of your projects and assets.&lt;/p&gt;
&lt;p&gt;Jenn Schiffer, formerly Director of Community at Glitch and then Fastly, is &lt;a href="https://livelaugh.blog/posts/on-important-changes-coming-to-glitch/"&gt;a little more salty&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;all that being said, i do sincerely want to thank fastly for giving glitch the opportunity to live to its 3-year acqui-versary this week. they generously took in a beautiful flower and placed it upon their sunny window sill with hopes to grow it more. the problem is they chose to never water it, and anyone with an elementary school education know what happens then. i wish us all a merry august earnings call season.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I'm very sad to see Glitch go. I've been pointing people to my tutorial on &lt;a href="https://simonwillison.net/2019/Apr/23/datasette-glitch/"&gt;Running Datasette on Glitch&lt;/a&gt; for 5 years now, it was a fantastic way to help people quickly get started hosting their own projects.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/s6utq0/saying_bye_glitch"&gt;lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/glitch"&gt;glitch&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fastly"&gt;fastly&lt;/a&gt;&lt;/p&gt;



</summary><category term="glitch"/><category term="datasette"/><category term="fastly"/></entry><entry><title>Over-engineering Secret Santa with Python cryptography and Datasette</title><link href="https://simonwillison.net/2022/Dec/11/over-engineering-secret-santa/#atom-tag" rel="alternate"/><published>2022-12-11T02:03:39+00:00</published><updated>2022-12-11T02:03:39+00:00</updated><id>https://simonwillison.net/2022/Dec/11/over-engineering-secret-santa/#atom-tag</id><summary type="html">
    &lt;p&gt;We're doing a family &lt;a href="https://en.wikipedia.org/wiki/Secret_Santa"&gt;Secret Santa&lt;/a&gt; this year, and we needed a way to randomly assign people to each other without anyone knowing who was assigned to who.&lt;/p&gt;
&lt;p&gt;I offered to write some software! (Maybe "insisted" is more accurate)&lt;/p&gt;
&lt;p&gt;I've been wanting an excuse to write something fun involving Python's &lt;a href="https://cryptography.io/en/latest/"&gt;cryptography&lt;/a&gt; library for years. The problem is that I'm too responsible/cowardly to ignore the many warnings to only use the "hazardous materials" area of that library if you know exactly what you're doing.&lt;/p&gt;
&lt;p&gt;A secret santa is the &lt;em&gt;perfect&lt;/em&gt; low stakes project to ignore those warnings and play with something fun.&lt;/p&gt;
&lt;h4&gt;My requirements&lt;/h4&gt;
&lt;p&gt;I have six participants. Each participant needs to know who they are to buy a gift for - with no way of finding out any of the other gift pairings.&lt;/p&gt;
&lt;p&gt;As the administrator of the system I must not be able to figure out the pairings either.&lt;/p&gt;
&lt;p&gt;I don't want to use email or logins or anything like that - I just want to be able to share a link in the family WhatsApp group and have everyone use the same interface to get their pairing.&lt;/p&gt;
&lt;h4&gt;How it works&lt;/h4&gt;
&lt;p&gt;Here's the scheme I came up with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each participant gets a password generated for them. This happens on demand when they click a button - with an honour system not to click someone else's button (easily detected since each button can only be clicked once). If someone DOES click someone else's button we can reset the entire system and start again.&lt;/li&gt;
&lt;li&gt;Their password is generated for them - it's three random words, for example "squirrel copper sailboat". I expect most people to take a screenshot with their phone to record it.&lt;/li&gt;
&lt;li&gt;Behind the scenes, each user has a RSA public/private key generated for them. The private key is encrypted using their new password, then both keys are stored in the database. The password itself is NOT stored.&lt;/li&gt;
&lt;li&gt;Once every user has generated and recorded their password, we can execute the Secret Santa assignments. This shuffles the participants and then assigns each person to the person after them in the list. It then uses their public keys to encrypt a message telling them who they should buy a gift for.&lt;/li&gt;
&lt;li&gt;Those encrypted messages are stored in the database too.&lt;/li&gt;
&lt;li&gt;Finally, each user can return to the site and enter their password to decrypt and view their message.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And here's an animated GIF demo:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/secret-santa.gif" alt="Animated GIF showing the plugin in action - the user adds three names, then gets the password for their account - then hits the assign button and uses their password to find out who they have been assigned" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4&gt;Building it as a Datasette plugin&lt;/h4&gt;
&lt;p&gt;This is a tiny app with a very small amount of persistence needed, so I decided to build it as a Datasette plugin on top of a couple of SQLite database tables.&lt;/p&gt;
&lt;p&gt;In addition to giving me an excuse to try something new with my &lt;a href="https://datasette.io/"&gt;main project&lt;/a&gt;, this should also hopefully make it easy to deploy.&lt;/p&gt;
&lt;p&gt;Most of the code is in the &lt;a href="https://github.com/simonw/datasette-secret-santa/blob/main/datasette_secret_santa/__init__.py"&gt;datasette_secret_santa/__init__.py&lt;/a&gt; file. I used a number of different &lt;a href="https://docs.datasette.io/en/stable/plugin_hooks.html"&gt;plugin hooks&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;startup()&lt;/code&gt; to create the database tables it needs when the server first starts (if they do not exist already)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;canned_queries()&lt;/code&gt; to add a canned SQL query for creating new Secret Santa groups, to save me from needing to build a custom UI for that&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;register_routes()&lt;/code&gt; to register five new custom pages within Datasette&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;extra_template_vars()&lt;/code&gt; to make an extra context variable available on the Datasette homepage, which is rendered using a custom template&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here are the routes:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;hookimpl&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;register_routes&lt;/span&gt;():
    &lt;span class="pl-k"&gt;return&lt;/span&gt; [
        (&lt;span class="pl-s"&gt;r"^/secret-santa/(?P&amp;lt;slug&amp;gt;[^/]+)$"&lt;/span&gt;, &lt;span class="pl-s1"&gt;secret_santa&lt;/span&gt;),
        (&lt;span class="pl-s"&gt;r"^/secret-santa/(?P&amp;lt;slug&amp;gt;[^/]+)/add$"&lt;/span&gt;, &lt;span class="pl-s1"&gt;add_participant&lt;/span&gt;),
        (&lt;span class="pl-s"&gt;r"^/secret-santa/(?P&amp;lt;slug&amp;gt;[^/]+)/assign$"&lt;/span&gt;, &lt;span class="pl-s1"&gt;assign_participants&lt;/span&gt;),
        (&lt;span class="pl-s"&gt;r"^/secret-santa/(?P&amp;lt;slug&amp;gt;[^/]+)/set-password/(?P&amp;lt;id&amp;gt;\d+)$"&lt;/span&gt;, &lt;span class="pl-s1"&gt;set_password&lt;/span&gt;),
        (&lt;span class="pl-s"&gt;r"^/secret-santa/(?P&amp;lt;slug&amp;gt;[^/]+)/reveal/(?P&amp;lt;id&amp;gt;\d+)$"&lt;/span&gt;, &lt;span class="pl-s1"&gt;reveal&lt;/span&gt;),
    ]&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/secret-santa/{slug}&lt;/code&gt; is the main page for a Secret Santa group. It shows a list of participants and a form to add a new participant.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/secret-santa/{slug}/add&lt;/code&gt; is the endpoint for a form that adds a new participant.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/secret-santa/{slug}/set-password/{id}&lt;/code&gt; is the page that lets a user generate and retrieve their password.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/secret-santa/{slug}/reveal/{id}&lt;/code&gt; is the page where a user enters their password to reveal their Secret Santa assignment.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/secret-santa/{slug}/assign&lt;/code&gt; is the endpoint that does the work of assigning participants to each other, and generating and saving encrypted message for each of them.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;The cryptography&lt;/h4&gt;
&lt;p&gt;The earlier warning holds firm here: I am not a cryptographer. I'm just having fun. You should not imitate any of the code I wrote here without thoroughly reviewing it with someone who knows what they're doing.&lt;/p&gt;
&lt;p&gt;(I also used ChatGPT to write my first drafts of it, as &lt;a href="https://github.com/simonw/datasette-secret-santa/issues/1#issuecomment-1345348032"&gt;described in this issue&lt;/a&gt;. Trusting cryptographic code generated by a large language model is a particularly bad idea!)&lt;/p&gt;
&lt;p&gt;Disclaimers out of the way, here's &lt;a href="https://github.com/simonw/datasette-secret-santa/blob/18995be276a0fff99cf2f788cc15ac409465231d/datasette_secret_santa/__init__.py#L246-L280"&gt;the code&lt;/a&gt; I wrote to generate and store the RSA keys:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;generate_password_and_keys_for_user&lt;/span&gt;(&lt;span class="pl-s1"&gt;db&lt;/span&gt;, &lt;span class="pl-s1"&gt;participant_id&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;password&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;" "&lt;/span&gt;.&lt;span class="pl-en"&gt;join&lt;/span&gt;(&lt;span class="pl-s1"&gt;random&lt;/span&gt;.&lt;span class="pl-en"&gt;sample&lt;/span&gt;(&lt;span class="pl-s1"&gt;words&lt;/span&gt;, &lt;span class="pl-c1"&gt;3&lt;/span&gt;))

    &lt;span class="pl-s1"&gt;private_key&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;rsa&lt;/span&gt;.&lt;span class="pl-en"&gt;generate_private_key&lt;/span&gt;(&lt;span class="pl-s1"&gt;public_exponent&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;65537&lt;/span&gt;, &lt;span class="pl-s1"&gt;key_size&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;2048&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;public_key&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;private_key&lt;/span&gt;.&lt;span class="pl-en"&gt;public_key&lt;/span&gt;()

    &lt;span class="pl-c"&gt;# Serialize the keys for storage&lt;/span&gt;
    &lt;span class="pl-s1"&gt;private_key_serialized&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;private_key&lt;/span&gt;.&lt;span class="pl-en"&gt;private_bytes&lt;/span&gt;(
        &lt;span class="pl-s1"&gt;encoding&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;serialization&lt;/span&gt;.&lt;span class="pl-v"&gt;Encoding&lt;/span&gt;.&lt;span class="pl-v"&gt;PEM&lt;/span&gt;,
        &lt;span class="pl-s1"&gt;format&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;serialization&lt;/span&gt;.&lt;span class="pl-v"&gt;PrivateFormat&lt;/span&gt;.&lt;span class="pl-v"&gt;PKCS8&lt;/span&gt;,
        &lt;span class="pl-s1"&gt;encryption_algorithm&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;serialization&lt;/span&gt;.&lt;span class="pl-v"&gt;BestAvailableEncryption&lt;/span&gt;(
            &lt;span class="pl-s1"&gt;password&lt;/span&gt;.&lt;span class="pl-en"&gt;encode&lt;/span&gt;(&lt;span class="pl-s"&gt;"utf-8"&lt;/span&gt;)
        ),
    ).&lt;span class="pl-en"&gt;decode&lt;/span&gt;(&lt;span class="pl-s"&gt;"utf-8"&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;public_key_serialized&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;public_key&lt;/span&gt;.&lt;span class="pl-en"&gt;public_bytes&lt;/span&gt;(
        &lt;span class="pl-s1"&gt;encoding&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;serialization&lt;/span&gt;.&lt;span class="pl-v"&gt;Encoding&lt;/span&gt;.&lt;span class="pl-v"&gt;PEM&lt;/span&gt;,
        &lt;span class="pl-s1"&gt;format&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;serialization&lt;/span&gt;.&lt;span class="pl-v"&gt;PublicFormat&lt;/span&gt;.&lt;span class="pl-v"&gt;SubjectPublicKeyInfo&lt;/span&gt;,
    ).&lt;span class="pl-en"&gt;decode&lt;/span&gt;(&lt;span class="pl-s"&gt;"utf-8"&lt;/span&gt;)

    &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-en"&gt;execute_write&lt;/span&gt;(
        &lt;span class="pl-s"&gt;"""&lt;/span&gt;
&lt;span class="pl-s"&gt;        update secret_santa_participants&lt;/span&gt;
&lt;span class="pl-s"&gt;        set&lt;/span&gt;
&lt;span class="pl-s"&gt;            password_issued_at = datetime('now'),&lt;/span&gt;
&lt;span class="pl-s"&gt;            public_key = :public_key,&lt;/span&gt;
&lt;span class="pl-s"&gt;            private_key = :private_key&lt;/span&gt;
&lt;span class="pl-s"&gt;        where id = :id&lt;/span&gt;
&lt;span class="pl-s"&gt;        """&lt;/span&gt;,
        {
            &lt;span class="pl-s"&gt;"id"&lt;/span&gt;: &lt;span class="pl-s1"&gt;participant_id&lt;/span&gt;,
            &lt;span class="pl-s"&gt;"public_key"&lt;/span&gt;: &lt;span class="pl-s1"&gt;public_key_serialized&lt;/span&gt;,
            &lt;span class="pl-s"&gt;"private_key"&lt;/span&gt;: &lt;span class="pl-s1"&gt;private_key_serialized&lt;/span&gt;,
        },
    )
    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;password&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, it uses &lt;code&gt;rsa.generate_private_key()&lt;/code&gt; from the &lt;a href="https://cryptography.io/en/latest/"&gt;PyCA cryptography library&lt;/a&gt; to generate the public and private keys.&lt;/p&gt;
&lt;p&gt;The options &lt;code&gt;public_exponent=65537, key_size=2048&lt;/code&gt; are recommended by the &lt;a href="https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key"&gt; generate_private_key() documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It then serializes them to PEM format strings that can be stored in the database.&lt;/p&gt;
&lt;p&gt;The private key is serialized after being encrypted using the randomly generated password for that user. This produces a string that looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-----BEGIN ENCRYPTED PRIVATE KEY-----
...
-----END ENCRYPTED PRIVATE KEY-----
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I had originally come up with &lt;a href="https://github.com/simonw/datasette-secret-santa/blob/cef3aed7afa523dc07190e7d735e46ecd61e4f5e/datasette_secret_santa/__init__.py#L269-L279"&gt;my own scheme for this&lt;/a&gt;, involving AES encryption and a key derived from a hash of the raw password (which I planned to later run through &lt;code&gt;bcrypt&lt;/code&gt; a few hundred thousand times) - I was very happy when &lt;a href="https://github.com/simonw/datasette-secret-santa/issues/3"&gt;I realized&lt;/a&gt; that there was a standard way to do this already.&lt;/p&gt;
&lt;p&gt;The code that then assigns the participants and generates their encrypted messages looks &lt;a href="https://github.com/simonw/datasette-secret-santa/blob/18995be276a0fff99cf2f788cc15ac409465231d/datasette_secret_santa/__init__.py#L311-L339"&gt;like this&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;# Assign participants&lt;/span&gt;
&lt;span class="pl-s1"&gt;random&lt;/span&gt;.&lt;span class="pl-en"&gt;shuffle&lt;/span&gt;(&lt;span class="pl-s1"&gt;participants&lt;/span&gt;)
&lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;i&lt;/span&gt;, &lt;span class="pl-s1"&gt;participant&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-en"&gt;enumerate&lt;/span&gt;(&lt;span class="pl-s1"&gt;participants&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;assigned&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;participants&lt;/span&gt;[(&lt;span class="pl-s1"&gt;i&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;) &lt;span class="pl-c1"&gt;%&lt;/span&gt; &lt;span class="pl-en"&gt;len&lt;/span&gt;(&lt;span class="pl-s1"&gt;participants&lt;/span&gt;)]
    &lt;span class="pl-s1"&gt;message&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;"You should buy a gift for {}"&lt;/span&gt;.&lt;span class="pl-en"&gt;format&lt;/span&gt;(&lt;span class="pl-s1"&gt;assigned&lt;/span&gt;[&lt;span class="pl-s"&gt;"name"&lt;/span&gt;])
    &lt;span class="pl-c"&gt;# Encrypt the message with their public key&lt;/span&gt;
    &lt;span class="pl-s1"&gt;public_key&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;serialization&lt;/span&gt;.&lt;span class="pl-en"&gt;load_pem_public_key&lt;/span&gt;(
        &lt;span class="pl-s1"&gt;participant&lt;/span&gt;[&lt;span class="pl-s"&gt;"public_key"&lt;/span&gt;].&lt;span class="pl-en"&gt;encode&lt;/span&gt;(&lt;span class="pl-s"&gt;"utf-8"&lt;/span&gt;), &lt;span class="pl-s1"&gt;backend&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-en"&gt;default_backend&lt;/span&gt;()
    )
    &lt;span class="pl-s1"&gt;secret_message_encrypted&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;public_key&lt;/span&gt;.&lt;span class="pl-en"&gt;encrypt&lt;/span&gt;(
        &lt;span class="pl-s1"&gt;message&lt;/span&gt;.&lt;span class="pl-en"&gt;encode&lt;/span&gt;(&lt;span class="pl-s"&gt;"utf-8"&lt;/span&gt;),
        &lt;span class="pl-s1"&gt;padding&lt;/span&gt;.&lt;span class="pl-v"&gt;OAEP&lt;/span&gt;(
            &lt;span class="pl-s1"&gt;mgf&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;padding&lt;/span&gt;.&lt;span class="pl-v"&gt;MGF1&lt;/span&gt;(&lt;span class="pl-s1"&gt;algorithm&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;hashes&lt;/span&gt;.&lt;span class="pl-v"&gt;SHA256&lt;/span&gt;()),
            &lt;span class="pl-s1"&gt;algorithm&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;hashes&lt;/span&gt;.&lt;span class="pl-v"&gt;SHA256&lt;/span&gt;(),
            &lt;span class="pl-s1"&gt;label&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;None&lt;/span&gt;,
        ),
    )
    &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-en"&gt;execute_write&lt;/span&gt;(
        &lt;span class="pl-s"&gt;"""&lt;/span&gt;
&lt;span class="pl-s"&gt;        update secret_santa_participants&lt;/span&gt;
&lt;span class="pl-s"&gt;        set secret_message_encrypted = :secret_message_encrypted&lt;/span&gt;
&lt;span class="pl-s"&gt;        where id = :id&lt;/span&gt;
&lt;span class="pl-s"&gt;        """&lt;/span&gt;,
        {
            &lt;span class="pl-s"&gt;"id"&lt;/span&gt;: &lt;span class="pl-s1"&gt;participant&lt;/span&gt;[&lt;span class="pl-s"&gt;"id"&lt;/span&gt;],
            &lt;span class="pl-s"&gt;"secret_message_encrypted"&lt;/span&gt;: &lt;span class="pl-s1"&gt;secret_message_encrypted&lt;/span&gt;,
        },
    )&lt;/pre&gt;
&lt;p&gt;And finally, the code that &lt;a href="https://github.com/simonw/datasette-secret-santa/blob/18995be276a0fff99cf2f788cc15ac409465231d/datasette_secret_santa/__init__.py#L201-L220"&gt;decrypts the message&lt;/a&gt; when the user provides their password again:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;data&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;request&lt;/span&gt;.&lt;span class="pl-en"&gt;post_vars&lt;/span&gt;()
&lt;span class="pl-s1"&gt;password&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;data&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"password"&lt;/span&gt;, &lt;span class="pl-s"&gt;""&lt;/span&gt;).&lt;span class="pl-en"&gt;strip&lt;/span&gt;()
&lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; &lt;span class="pl-s1"&gt;password&lt;/span&gt;:
    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;_error&lt;/span&gt;(
        &lt;span class="pl-s1"&gt;datasette&lt;/span&gt;, &lt;span class="pl-s1"&gt;request&lt;/span&gt;, &lt;span class="pl-s"&gt;"Please provide a password"&lt;/span&gt;, &lt;span class="pl-s1"&gt;status&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;400&lt;/span&gt;
    )
&lt;span class="pl-c"&gt;# Decrypt the private key with the password&lt;/span&gt;
&lt;span class="pl-k"&gt;try&lt;/span&gt;:
    &lt;span class="pl-s1"&gt;private_key&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;decrypt_private_key_for_user&lt;/span&gt;(&lt;span class="pl-s1"&gt;participant&lt;/span&gt;, &lt;span class="pl-s1"&gt;password&lt;/span&gt;)
&lt;span class="pl-k"&gt;except&lt;/span&gt; &lt;span class="pl-v"&gt;ValueError&lt;/span&gt;:
    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;_error&lt;/span&gt;(&lt;span class="pl-s1"&gt;datasette&lt;/span&gt;, &lt;span class="pl-s1"&gt;request&lt;/span&gt;, &lt;span class="pl-s"&gt;"Incorrect password"&lt;/span&gt;, &lt;span class="pl-s1"&gt;status&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;400&lt;/span&gt;)
&lt;span class="pl-c"&gt;# Decrypt the secret message with the private key&lt;/span&gt;
&lt;span class="pl-s1"&gt;decrypted_message&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;private_key&lt;/span&gt;.&lt;span class="pl-en"&gt;decrypt&lt;/span&gt;(
    &lt;span class="pl-s1"&gt;participant&lt;/span&gt;[&lt;span class="pl-s"&gt;"secret_message_encrypted"&lt;/span&gt;],
    &lt;span class="pl-s1"&gt;padding&lt;/span&gt;.&lt;span class="pl-v"&gt;OAEP&lt;/span&gt;(
        &lt;span class="pl-s1"&gt;mgf&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;padding&lt;/span&gt;.&lt;span class="pl-v"&gt;MGF1&lt;/span&gt;(&lt;span class="pl-s1"&gt;algorithm&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;hashes&lt;/span&gt;.&lt;span class="pl-v"&gt;SHA256&lt;/span&gt;()),
        &lt;span class="pl-s1"&gt;algorithm&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;hashes&lt;/span&gt;.&lt;span class="pl-v"&gt;SHA256&lt;/span&gt;(),
        &lt;span class="pl-s1"&gt;label&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;None&lt;/span&gt;,
    ),
).&lt;span class="pl-en"&gt;decode&lt;/span&gt;(&lt;span class="pl-s"&gt;"utf-8"&lt;/span&gt;)&lt;/pre&gt;
&lt;h3&gt;And some snowflakes&lt;/h3&gt;
&lt;p&gt;I spent all of five minutes on the visual design for it - the main feature of which is a thick red top border on body followed by a thinner white border to make it look like its wearing a Santa hat.&lt;/p&gt;
&lt;p&gt;I did add some animated snowflakes though! I used &lt;a href="https://github.com/natbat/CSS-Snow"&gt;this script&lt;/a&gt; Natalie Downe built back in 2010. It works great!&lt;/p&gt;
&lt;h4&gt;Deploying it on Glitch&lt;/h4&gt;
&lt;p&gt;This kind of project is a really great fit for &lt;a href="https://glitch.com/"&gt;Glitch&lt;/a&gt;, which offers free hosting with persistent file storage - perfect for SQLite - provided you don't mind your projects going to sleep in between bouts of activity (unless you pay to "boost" them). A Secret Santa app is a perfect fit for this sort of hosting.&lt;/p&gt;
&lt;p&gt;(You can &lt;a href="https://glitch.com/~datasette-secret-santa"&gt;remix my project&lt;/a&gt; to get your own copy of the app (with your own database) by clicking the "Remix" button.)&lt;/p&gt;
&lt;p&gt;Since I had &lt;a href="https://pypi.org/project/datasette-secret-santa"&gt;shipped the plugin&lt;/a&gt; up to PyPI already, deploying it on Glitch was a matter of creating a new project there containing this single &lt;code&gt;glitch.json&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;{
  &lt;span class="pl-ent"&gt;"install"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;pip3 install --user datasette datasette-secret-santa -U&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-ent"&gt;"start"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;datasette --create .data/santa.db -p 3000&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This causes Glitch to install both &lt;code&gt;datasette&lt;/code&gt; and &lt;code&gt;datasette-secret-santa&lt;/code&gt; when the project first launches. It then starts the Datasette server running like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette --create .data/santa.db -p 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;--create&lt;/code&gt; flag tells Datasette to create a new SQLite database if one doesn't already exist at that path. &lt;code&gt;.data/&lt;/code&gt; is a &lt;a href="https://glitch.happyfox.com/kb/article/22-do-you-have-built-in-persistence-or-a-database/"&gt;special directory&lt;/a&gt; on Glitch that won't have its contents automatically tracked using their version control.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;-p 3000&lt;/code&gt; flag tells the server to listen on port 3000, which is the Glitch default - traffic to the subdomain for the app will automatically be routed to that port.&lt;/p&gt;
&lt;h4&gt;And the database is public&lt;/h4&gt;
&lt;p&gt;Here's a slightly surprising thing about this: the SQLite table containing all of the data - including the public keys and encrypted private keys - is visible for anyone with access to the instance to see!&lt;/p&gt;
&lt;p&gt;&lt;a href="http://datasette-secret-santa.glitch.me/santa/secret_santa_participants"&gt;Here's that table&lt;/a&gt; for a demo I deployed on Glitch.&lt;/p&gt;
&lt;p&gt;Once again, I am by no means a cryptography expert, and this isn't something I would tolerate for any other application. But with the risk profile involved in a secret santa I think this is OK. I'm pretty sure you could brute force decrypt the private keys if you really wanted to, so it's a good thing they're not being used for anything else!&lt;/p&gt;
&lt;p&gt;(This is also one of the reasons I didn't let users pick their own passwords - by assigning generated passwords I can be 100% sure I don't accidentally end up holding onto an encrypted copy of a credential that could be used for anything else.)&lt;/p&gt;
&lt;h4&gt;Self-contained apps as plugins&lt;/h4&gt;
&lt;p&gt;Something I find interesting about this project is that it demonstrates how a Datasette plugin can be used to provide a full, self-contained app.&lt;/p&gt;
&lt;p&gt;I think this is a powerful pattern. It's a neat way to take advantage of the tools I've built to help make Datasette easy to deploy - not just on Glitch but &lt;a href="https://simonwillison.net/2022/Feb/15/fly-volumes/"&gt;on platforms like Fly&lt;/a&gt; as well.&lt;/p&gt;
&lt;p&gt;This is my first time using Datasette in this way and I found it to be a pleasantly productive way of building and deploying this kind of personal tool. I'm looking forward to trying this approach out for other projects in the future.&lt;/p&gt;
&lt;p&gt;And if you know cryptography and can spot any glaring (or subtle) holes in the way my system works, please &lt;a href="https://github.com/simonw/datasette-secret-santa/issues/new"&gt;open an issue&lt;/a&gt; and let me know!&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cryptography"&gt;cryptography&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/glitch"&gt;glitch&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chatgpt"&gt;chatgpt&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;/p&gt;
    

</summary><category term="cryptography"/><category term="glitch"/><category term="projects"/><category term="datasette"/><category term="chatgpt"/><category term="llms"/><category term="ai-assisted-programming"/></entry><entry><title>Datasette 0.31</title><link href="https://simonwillison.net/2019/Nov/12/datasette/#atom-tag" rel="alternate"/><published>2019-11-12T06:11:57+00:00</published><updated>2019-11-12T06:11:57+00:00</updated><id>https://simonwillison.net/2019/Nov/12/datasette/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://datasette.readthedocs.io/en/stable/changelog.html#v0-31"&gt;Datasette 0.31&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Released today: this version adds compatibility with Python 3.8 and breaks compatibility with Python 3.5. Since Glitch support Python 3.7.3 now I decided I could finally give up on 3.5. This means Datasette can use f-strings now, but more importantly it opens up the opportunity to start taking advantage of Starlette, which makes all kinds of interesting new ASGI-based plugins much easier to build.


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



</summary><category term="glitch"/><category term="projects"/><category term="python"/><category term="datasette"/><category term="asgi"/><category term="starlette"/></entry><entry><title>Weeknotes: Python 3.7 on Glitch, datasette-render-markdown</title><link href="https://simonwillison.net/2019/Nov/11/weeknotes-8/#atom-tag" rel="alternate"/><published>2019-11-11T23:26:34+00:00</published><updated>2019-11-11T23:26:34+00:00</updated><id>https://simonwillison.net/2019/Nov/11/weeknotes-8/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;a href="https://simonwillison.net/2019/Oct/28/niche-museums-kepler/#Streaks_56"&gt;Streaks&lt;/a&gt; is really working well for me. I’m at 12 days of commits to &lt;a href="https://github.com/simonw/datasette"&gt;Datasette&lt;/a&gt;, 16 posting a daily &lt;a href="https://www.niche-museums.com/"&gt;Niche Museum&lt;/a&gt;, 19 of actually reviewing my email inbox and 14 of guitar practice. I rewarded myself for that last one by purchasing an actual classical (as opposed to acoustic) guitar.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Datasette_4"&gt;&lt;/a&gt;Datasette&lt;/h3&gt;
&lt;p&gt;One downside: since my aim is to land a commit to Datasette master every day, I’m incentivised to land small changes. I have a bunch of much larger Datasette projects in the works - I think my goal for the next week should be to land one of those. Contenders include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette/issues/617"&gt;TableView.data()&lt;/a&gt; refactor - a blocker on a bunch of other projects&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette/issues/567"&gt;Datasette Edit&lt;/a&gt; - finish &lt;a href="https://github.com/simonw/datasette/issues/569"&gt;the new connection work&lt;/a&gt; so I can have plugins that write changes to databases&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/datasette/issues/417"&gt;Datasette Library&lt;/a&gt; - watch a directory and automatically serve new database files that show up in that directory&lt;/li&gt;
&lt;li&gt;Finish and ship my work on &lt;a href="https://github.com/simonw/datasette/issues/551"&gt;facet-by-many-to-many&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Implement &lt;a href="https://github.com/simonw/datasette/issues/613"&gt;basic join support&lt;/a&gt; for table views (so you can join without writing a custom SQL query)&lt;/li&gt;
&lt;li&gt;Probably the most impactful: Datasette needs a website! Up until now I’ve directed people to &lt;a href="https://github.com/simonw/datasette"&gt;GitHub&lt;/a&gt; or to &lt;a href="https://datasette.readthedocs.io/"&gt;the documentation&lt;/a&gt; but the project has grow to the point that it warrants its own home.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I’m going to redefine my daily goal to include pushing in-progress work to Datasette branches in an attempt to escape that false incentive.&lt;/p&gt;
&lt;h3&gt;&lt;a id="New_datasettecsvs_using_Python_37_on_Glitch_17"&gt;&lt;/a&gt;New datasette-csvs using Python 3.7 on Glitch&lt;/h3&gt;
&lt;p&gt;The main reason I’ve been strict about keeping Datasette compatible with Python 3.5 is that it was the only version supported by &lt;a href="https://glitch.com/"&gt;Glitch&lt;/a&gt;, and Glitch has become my favourite tool for getting people &lt;a href="https://datasette.readthedocs.io/en/latest/getting_started.html#try-datasette-without-installing-anything-using-glitch"&gt;up and running with Datasette quickly&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There’s been a long running &lt;a href="https://support.glitch.com/t/can-you-upgrade-python-to-latest-version/7980"&gt;Glitch support thread&lt;/a&gt; requesting an upgrade, and last week it finally bore fruit. Projects on Glitch now get &lt;code&gt;python3&lt;/code&gt; pointing to Python 3.7.5 instead!&lt;/p&gt;
&lt;p&gt;This actually broke my &lt;a href="https://glitch.com/~datasette-csvs"&gt;datasette-csvs&lt;/a&gt; project at first, because for some reason under Python 3.7 the Pandas dependency used by &lt;a href="https://github.com/simonw/csvs-to-sqlite"&gt;csvs-to-sqlite&lt;/a&gt; started taking up too much space from the 200MB Glitch instance quota. I ended up working around this by switching over to using my &lt;a href="https://sqlite-utils.readthedocs.io/"&gt;sqlite-utils&lt;/a&gt; CLI tool instead, which has much lighter dependencies.&lt;/p&gt;
&lt;p&gt;I’ve shared the new code for my Glitch project in &lt;a href="https://sqlite-utils.readthedocs.io/en/stable/cli.html#inserting-csv-or-tsv-data"&gt;the datasette-csvs repo&lt;/a&gt; on GitHub.&lt;/p&gt;
&lt;p&gt;The one thing missing from &lt;code&gt;sqlite-utils insert my.db mytable myfile.csv --csv&lt;/code&gt; right now is the ability to run it against multiple files at once - something &lt;code&gt;csvs-to-sqlite&lt;/code&gt; handles really well. I ended up finally learning how to use &lt;code&gt;while&lt;/code&gt; in bash and wrote the following &lt;a href="http://install.sh"&gt;install.sh&lt;/a&gt; shell script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ pip3 install -U -r requirements.txt --user &amp;amp;&amp;amp; \
  mkdir -p .data &amp;amp;&amp;amp; \
  rm .data/data.db || true &amp;amp;&amp;amp; \
  for f in *.csv
    do
        sqlite-utils insert .data/data.db ${f%.*} $f --csv
    done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;${f%.*}&lt;/code&gt; is the bash incantation for stripping off the file extension - so the above evaluates to this for each of the CSV files it finds in the root directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sqlite-utils insert .data/data.db trees trees.csv --csv
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a id="githubtosqlite_releases_42"&gt;&lt;/a&gt;github-to-sqlite releases&lt;/h3&gt;
&lt;p&gt;I released &lt;a href="https://github.com/dogsheep/github-to-sqlite/releases/tag/0.6"&gt;github-to-sqlite 0.6&lt;/a&gt; with a new sub-command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ github-to-sqlite releases github.db simonw/datasette
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It grabs all of the releases for a repository using &lt;a href="https://developer.github.com/v3/repos/releases/#list-releases-for-a-repository"&gt;the GitHub releases API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I’m using this for my personal Dogsheep instance, but I’m also planning to use this for the forthcoming Datasette website - I want to pull together all of the releases of all of &lt;a href="https://datasette.readthedocs.io/en/latest/ecosystem.html"&gt;the Datasette Ecosystem&lt;/a&gt; of projects in one place.&lt;/p&gt;
&lt;p&gt;I decided to exercise my new bash &lt;code&gt;while&lt;/code&gt; skills and write a script to run by cron once an hour which fetches all of my repos (from both my &lt;code&gt;simonw&lt;/code&gt; account and my &lt;code&gt;dogsheep&lt;/code&gt; GitHub organization) and then fetches their releases.&lt;/p&gt;
&lt;p&gt;Since I don’t want to fetch releases for all 257 of my personal GitHub repos - just the repos which relate to Datasette - I started applying a new &lt;a href="https://github.com/topics/datasette-io"&gt;datasette-io topic&lt;/a&gt; (for &lt;a href="https://datasette.io/"&gt;datasette.io&lt;/a&gt;, my planned website domain) to the repos that I want to pull releases from.&lt;/p&gt;
&lt;p&gt;Then I came up with this shell script monstrosity:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
# Fetch repos for simonw and dogsheep
github-to-sqlite repos github.db simonw dogsheep -a auth.json

# Fetch releases for the repos tagged 'datasette-io'
sqlite-utils github.db &amp;quot;
select full_name from repos where rowid in (
    select repos.rowid from repos, json_each(repos.topics) j
    where j.value = 'datasette-io'
)&amp;quot; --csv --no-headers | while read repo;
    do github-to-sqlite releases \
            github.db $(echo $repo | tr -d '\r') \
            -a auth.json;
        sleep 2;
    done;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here’s an example of the database this produces, running on Cloud Run: &lt;a href="https://github-to-sqlite-releases-j7hipcg4aq-uc.a.run.app"&gt;https://github-to-sqlite-releases-j7hipcg4aq-uc.a.run.app&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I’m using the ability of &lt;code&gt;sqlite-utils&lt;/code&gt; to run a SQL query and return the results as CSV, but without the header row. Then I pipe the results through a &lt;code&gt;while&lt;/code&gt; loop and use them to call the &lt;code&gt;github-to-sqlite releases&lt;/code&gt; command against each repo.&lt;/p&gt;
&lt;p&gt;I ran into a weird bug which turned out to be caused by the CSV output using &lt;code&gt;\r\n&lt;/code&gt; which was fed into &lt;code&gt;github-to-sqlite releases&lt;/code&gt; as &lt;code&gt;simonw/datasette\r&lt;/code&gt; - I fixed that using &lt;code&gt;$(echo $repo | tr -d '\r')&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;&lt;a id="datasetterendermarkdown_81"&gt;&lt;/a&gt;datasette-render-markdown&lt;/h3&gt;
&lt;p&gt;Now that I have a &lt;code&gt;releases&lt;/code&gt; database table with all of the releases of my various packages I want to be able to browse them in one place. I &lt;a href="https://github-to-sqlite-releases-j7hipcg4aq-uc.a.run.app/github/releases"&gt;fired up Datasette&lt;/a&gt; and realized that the most interesting information is in the &lt;code&gt;body&lt;/code&gt; column, which contains markdown.&lt;/p&gt;
&lt;p&gt;So I built a plugin for the &lt;a href="https://datasette.readthedocs.io/en/stable/plugins.html#render-cell-value-column-table-database-datasette"&gt;render_cell plugin hook&lt;/a&gt; which safely renders markdown data as HTML. Here’s &lt;a href="https://github.com/simonw/datasette-render-markdown/blob/579f99bbd725f553c7d60d6cf8bb317ea09d5ef2/datasette_render_markdown/__init__.py"&gt;the full implementation&lt;/a&gt; of the plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import bleach
import markdown
from datasette import hookimpl
import jinja2

ALLOWED_TAGS = [
    &amp;quot;a&amp;quot;, &amp;quot;abbr&amp;quot;, &amp;quot;acronym&amp;quot;, &amp;quot;b&amp;quot;, &amp;quot;blockquote&amp;quot;, &amp;quot;code&amp;quot;, &amp;quot;em&amp;quot;,
    &amp;quot;i&amp;quot;, &amp;quot;li&amp;quot;, &amp;quot;ol&amp;quot;, &amp;quot;strong&amp;quot;, &amp;quot;ul&amp;quot;, &amp;quot;pre&amp;quot;, &amp;quot;p&amp;quot;, &amp;quot;h1&amp;quot;,&amp;quot;h2&amp;quot;,
    &amp;quot;h3&amp;quot;, &amp;quot;h4&amp;quot;, &amp;quot;h5&amp;quot;, &amp;quot;h6&amp;quot;,
]

@hookimpl()
def render_cell(value, column):
    if not isinstance(value, str):
        return None
    # Only convert to markdown if table ends in _markdown
    if not column.endswith(&amp;quot;_markdown&amp;quot;):
        return None
    # Render it!
    html = bleach.linkify(
        bleach.clean(
            markdown.markdown(value, output_format=&amp;quot;html5&amp;quot;),
            tags=ALLOWED_TAGS,
        )
    )
    return jinja2.Markup(html)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This first release of the plugin just looks for column names that end in &lt;code&gt;_markdown&lt;/code&gt; and renders those. So the following SQL query does what I need:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select
  json_object(&amp;quot;label&amp;quot;, repos.full_name, &amp;quot;href&amp;quot;, repos.html_url) as repo,
  json_object(
    &amp;quot;href&amp;quot;,
    releases.html_url,
    &amp;quot;label&amp;quot;,
    releases.name
  ) as release,
  substr(releases.published_at, 0, 11) as date,
  releases.body as body_markdown,
  releases.published_at
from
  releases
  join repos on repos.id = releases.repo
order by
  releases.published_at desc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In aliases &lt;code&gt;releases.body&lt;/code&gt; to &lt;code&gt;body_markdown&lt;/code&gt; to trigger the markdown rendering, and uses &lt;code&gt;json_object(...)&lt;/code&gt; to cause &lt;a href="https://github.com/simonw/datasette-json-html"&gt;datasette-json-html&lt;/a&gt; to render some links.&lt;/p&gt;
&lt;p&gt;You can &lt;a href="https://github-to-sqlite-releases-j7hipcg4aq-uc.a.run.app/github?sql=select%0D%0A++json_object%28%22label%22%2C+repos.full_name%2C+%22href%22%2C+repos.html_url%29+as+repo%2C%0D%0A++json_object%28%0D%0A++++%22href%22%2C%0D%0A++++releases.html_url%2C%0D%0A++++%22label%22%2C%0D%0A++++releases.name%0D%0A++%29+as+release%2C%0D%0A++substr%28releases.published_at%2C+0%2C+11%29+as+date%2C%0D%0A++releases.body+as+body_markdown%2C%0D%0A++releases.published_at%0D%0Afrom%0D%0A++releases%0D%0A++join+repos+on+repos.id+%3D+releases.repo%0D%0Aorder+by%0D%0A++releases.published_at+desc"&gt;see the results here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2019/releases.png" alt="Releases SQL results" style="max-width: 100%" /&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a id="More_museums_140"&gt;&lt;/a&gt;More museums&lt;/h3&gt;
&lt;p&gt;I added &lt;a href="https://www.niche-museums.com/museums?sql=select+id%2C+json_object%28%22img_src%22%2C+photo_url+%7C%7C+%22%3Fw%3D800%26h%3D400%26fit%3Dcrop%22%2C+%22width%22%2C+400%29+as+img%2C%0D%0Aname%2C+url%2C+description%2C+address%2C+latitude%2C+longitude+from+museums%0D%0Awhere+id+in+%2826%2C27%2C28%2C29%2C30%2C31%2C32%29+order+by+id"&gt;another 7 museums&lt;/a&gt; to &lt;a href="https://www.niche-museums.com/"&gt;www.niche-museums.com&lt;/a&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dingles Fairground Heritage Centre&lt;/li&gt;
&lt;li&gt;Ilfracombe Museum&lt;/li&gt;
&lt;li&gt;Barometer World&lt;/li&gt;
&lt;li&gt;La Galcante&lt;/li&gt;
&lt;li&gt;Musée des Arts et Métiers&lt;/li&gt;
&lt;li&gt;International Women’s Air &amp;amp; Space Museum&lt;/li&gt;
&lt;li&gt;West Kern Oil Museum&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/glitch"&gt;glitch&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/museums"&gt;museums&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/markdown"&gt;markdown&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="glitch"/><category term="museums"/><category term="python"/><category term="markdown"/><category term="datasette"/><category term="weeknotes"/><category term="sqlite-utils"/></entry><entry><title>Get your own Pocket OAuth token</title><link href="https://simonwillison.net/2019/Oct/5/get-your-own-pocket-oauth-token/#atom-tag" rel="alternate"/><published>2019-10-05T21:56:38+00:00</published><updated>2019-10-05T21:56:38+00:00</updated><id>https://simonwillison.net/2019/Oct/5/get-your-own-pocket-oauth-token/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://your-pocket-oauth-token.glitch.me/"&gt;Get your own Pocket OAuth token&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I hate it when APIs make you jump through extensive hoops just to get an access token for pulling data directly from your own personal account. I’ve been playing with the Pocket API today and it has a pretty complex OAuth flow, so I built a tiny Flask app on Glitch which helps go through the steps to get an API token for your own personal Pocket account.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://gist.github.com/simonw/cfbec5ee35738ee2d5f6459eb92b1925"&gt;Source code for your-pocket-oauth-token.glitch.me&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/glitch"&gt;glitch&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mozilla"&gt;mozilla&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/oauth"&gt;oauth&lt;/a&gt;&lt;/p&gt;



</summary><category term="glitch"/><category term="mozilla"/><category term="oauth"/></entry><entry><title>Public Data Release of Stack Overflow’s 2019 Developer Survey</title><link href="https://simonwillison.net/2019/May/21/public-data-release-of-stack-overflows-2019-developer-survey/#atom-tag" rel="alternate"/><published>2019-05-21T18:51:43+00:00</published><updated>2019-05-21T18:51:43+00:00</updated><id>https://simonwillison.net/2019/May/21/public-data-release-of-stack-overflows-2019-developer-survey/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://stackoverflow.blog/2019/05/21/public-data-release-of-stack-overflows-2019-developer-survey/"&gt;Public Data Release of Stack Overflow’s 2019 Developer Survey&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Here’s the Stack Overflow announcement of their developer survey public data release, which discusses the Glitch partnership and mentions Datasette.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/glitch"&gt;glitch&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/stackoverflow"&gt;stackoverflow&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/surveys"&gt;surveys&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;



</summary><category term="glitch"/><category term="stackoverflow"/><category term="surveys"/><category term="datasette"/></entry><entry><title>Discover Insights in Developer Survey Results</title><link href="https://simonwillison.net/2019/May/21/discover-insights-developer-survey-results/#atom-tag" rel="alternate"/><published>2019-05-21T18:50:22+00:00</published><updated>2019-05-21T18:50:22+00:00</updated><id>https://simonwillison.net/2019/May/21/discover-insights-developer-survey-results/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://glitch.com/culture/discover-insights-explore-developer-survey-results-2019/"&gt;Discover Insights in Developer Survey Results&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Stack Overflow partnered with Glitch and used Datasette to host the full data set from Stack Overflow’s 2019 Developer Survey!


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/glitch"&gt;glitch&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/stackoverflow"&gt;stackoverflow&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/surveys"&gt;surveys&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;



</summary><category term="glitch"/><category term="stackoverflow"/><category term="surveys"/><category term="datasette"/></entry><entry><title>Hello world for ASGI running on Glitch</title><link href="https://simonwillison.net/2019/Apr/26/hello-world-asgi-running-glitch/#atom-tag" rel="alternate"/><published>2019-04-26T05:06:12+00:00</published><updated>2019-04-26T05:06:12+00:00</updated><id>https://simonwillison.net/2019/Apr/26/hello-world-asgi-running-glitch/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://glitch.com/~asgi"&gt;Hello world for ASGI running on Glitch&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I’m continuing to experiment with Python 3 running on Glitch. This evening on my walk home from work I built this “hello world” demo on my phone, partly to see if Glitch was a workable mobile development environment—it passed with flying colours! The demo is a simple hello world implemented using the new ASGI 3.0 specification, running on the daphne reference server. Click the “via” link for my accompanying thread on Twitter, which includes a short screencast (also recorded on my phone) showing Glitch in action.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/glitch"&gt;glitch&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/asgi"&gt;asgi&lt;/a&gt;&lt;/p&gt;



</summary><category term="glitch"/><category term="projects"/><category term="asgi"/></entry><entry><title>Language support on Glitch: a list</title><link href="https://simonwillison.net/2019/Apr/23/language-support-glitch-list/#atom-tag" rel="alternate"/><published>2019-04-23T16:28:35+00:00</published><updated>2019-04-23T16:28:35+00:00</updated><id>https://simonwillison.net/2019/Apr/23/language-support-glitch-list/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://support.glitch.com/t/language-support-on-glitch-a-list/5466"&gt;Language support on Glitch: a list&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is really useful: it’s essentially “Glitch: the missing manual” for running languages other than JavaScript. The Glitch community forums are a gold mine of useful information like this.

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


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



</summary><category term="glitch"/></entry><entry><title>Running Datasette on Glitch</title><link href="https://simonwillison.net/2019/Apr/23/datasette-glitch/#atom-tag" rel="alternate"/><published>2019-04-23T04:08:53+00:00</published><updated>2019-04-23T04:08:53+00:00</updated><id>https://simonwillison.net/2019/Apr/23/datasette-glitch/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;em&gt;&lt;strong&gt;Update 28th May 2025&lt;/strong&gt;: Sadly Glitch &lt;a href="https://blog.glitch.com/post/changes-are-coming-to-glitch/"&gt;is shutting down user project hosting&lt;/a&gt;, so this tutorial is no longer relevant&lt;/em&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;The worst part of any software project is setting up a development environment. It’s by far the biggest barrier for anyone trying to get started learning to code. I’ve been a developer for more than twenty years and I still feel the pain any time I want to do something new.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://glitch.com/"&gt;Glitch&lt;/a&gt; is the most promising attempt I’ve ever seen at tackling this problem. It provides an entirely browser-based development environment that allows you to edit code, see the results instantly and view and remix the source code of other people’s projects.&lt;/p&gt;
&lt;p&gt;It’s developed into a really fun, super-creative community and a fantastic resource for people looking to get started in the ever-evolving world of software development.&lt;/p&gt;
&lt;p&gt;This evening I decided to get &lt;a href="https://datasette.readthedocs.io/"&gt;Datasette&lt;/a&gt; running on it. I’m really impressed with how well it works, and I think Glitch provides an excellent environment for experimenting with Datasette and &lt;a href="https://datasette.readthedocs.io/en/stable/ecosystem.html"&gt;related tools&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;TLDR version: visit &lt;a href="https://glitch.com/edit/#!/remix/datasette-csvs"&gt;https://glitch.com/edit/#!/remix/datasette-csvs&lt;/a&gt; right now, drag-and-drop in a CSV file and watch it get served by Datasette on Glitch just a few seconds later.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Running_Python_on_Glitch_12"&gt;&lt;/a&gt;Running Python on Glitch&lt;/h3&gt;
&lt;p&gt;The Glitch documentation is all about Node.js and JavaScript, but they actually have very solid Python support as well.&lt;/p&gt;
&lt;p&gt;Every Glitch project runs in a container that includes Python 2.7.12 and Python 3.5.2, and you can use &lt;code&gt;pip install --user&lt;/code&gt; or &lt;code&gt;pip3 install --user&lt;/code&gt; to install Python dependencies.&lt;/p&gt;
&lt;p&gt;The key to running non-JavaScript projects on Glitch is the &lt;code&gt;glitch.json&lt;/code&gt; file format. You can use this to specify an &lt;code&gt;install&lt;/code&gt; script, which sets up your container, and a &lt;code&gt;start&lt;/code&gt; script, which starts your application running. Glitch will route HTTP traffic to port 3000, so your application server needs to listen on that port.&lt;/p&gt;
&lt;p&gt;This means the most basic Glitch project to run Datasette looks like this:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://datasette-basic.glitch.me/"&gt;https://datasette-basic.glitch.me/&lt;/a&gt; (&lt;a href="https://glitch.com/edit/#!/datasette-basic"&gt;view source&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;It contains a single &lt;code&gt;glitch.json&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &amp;quot;install&amp;quot;: &amp;quot;pip3 install --user datasette&amp;quot;,
    &amp;quot;start&amp;quot;: &amp;quot;datasette -p 3000&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This installs Datasette using &lt;code&gt;pip3&lt;/code&gt;, then runs it on port 3000.&lt;/p&gt;
&lt;p&gt;Since there’s no actual data to serve, this is a pretty boring demo. The most interesting page is this one, which shows the installed versions of the software:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://datasette-basic.glitch.me/-/versions"&gt;https://datasette-basic.glitch.me/-/versions&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;&lt;a id="Something_more_interesting_datasettecsvs_37"&gt;&lt;/a&gt;Something more interesting: datasette-csvs&lt;/h3&gt;
&lt;p&gt;Let’s build one with some actual data.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://github.com/simonw/csvs-to-sqlite"&gt;csvs-to-sqlite&lt;/a&gt; tool converts CSV files into a SQLite database. Since it’s also written in Python we can run it against CSV files as part of the Glitch install script.&lt;/p&gt;
&lt;p&gt;Glitch provides a special directory called &lt;code&gt;.data/&lt;/code&gt; which can be used as a persistent file storage space that won’t be cleared in between restarts. The following &lt;code&gt;&amp;quot;install&amp;quot;&lt;/code&gt; script installs &lt;code&gt;datasette&lt;/code&gt; and &lt;code&gt;csvs-to-sqlite&lt;/code&gt;, then runs the latter to create a SQLite database from all available CSV files:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &amp;quot;install&amp;quot;:  &amp;quot;pip3 install --user datasette csvs-to-sqlite &amp;amp;&amp;amp; csvs-to-sqlite *.csv .data/csv-data.db&amp;quot;,
    &amp;quot;start&amp;quot;: &amp;quot;datasette .data/csv-data.db -p 3000&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can simply drag and drop CSV files into the root of the Glitch project and they will be automatically converted into a SQLite database and served using Datasette!&lt;/p&gt;
&lt;p&gt;We need a couple of extra details. Firstly, we want Datasette to automatically re-build the database file any time a new CSV file is added or an existing CSV file is changed. We can do that by adding a &lt;code&gt;&amp;quot;watch&amp;quot;&lt;/code&gt; block to &lt;code&gt;glitch.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;watch&amp;quot;: {
    &amp;quot;install&amp;quot;: {
        &amp;quot;include&amp;quot;: [
            &amp;quot;\\.csv$&amp;quot;
        ]
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures that our &lt;code&gt;&amp;quot;install&amp;quot;&lt;/code&gt; script will run again any time a CSV file changes.&lt;/p&gt;
&lt;p&gt;Let’s tone down the rate at which the scripts execute, by using &lt;code&gt;throttle&lt;/code&gt; to set the polling interval to once a second:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;quot;throttle&amp;quot;: 1000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above almost worked, but I started seeing errors if I changed the number of columns in a CSV file, since doing so clashed with the schema that had already been created in the database.&lt;/p&gt;
&lt;p&gt;My solution was to add code to the install script that would delete the SQLite database file before attempting to recreate it - using the &lt;code&gt;rm ... || true&lt;/code&gt; idiom to prevent Glitch from failing the installation if the file it attempted to remove did not already exist.&lt;/p&gt;
&lt;p&gt;My final &lt;code&gt;glitch.json&lt;/code&gt; file looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &amp;quot;install&amp;quot;: &amp;quot;pip3 install --user datasette csvs-to-sqlite &amp;amp;&amp;amp; rm .data/csv-data.db || true &amp;amp;&amp;amp; csvs-to-sqlite *.csv .data/csv-data.db&amp;quot;,
  &amp;quot;start&amp;quot;: &amp;quot;datasette .data/csv-data.db -p 3000 -m metadata.json&amp;quot;,
  &amp;quot;watch&amp;quot;: {
    &amp;quot;install&amp;quot;: {
      &amp;quot;include&amp;quot;: [
        &amp;quot;\\.csv$&amp;quot;
      ]
    },
    &amp;quot;restart&amp;quot;: {
      &amp;quot;include&amp;quot;: [
        &amp;quot;^metadata.json$&amp;quot;
      ]
    },
    &amp;quot;throttle&amp;quot;: 1000
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also set it up to use &lt;a href="https://datasette.readthedocs.io/en/stable/metadata.html"&gt;Datasette’s metadata.json format&lt;/a&gt;, and automatically restart the server any time the contents of that file changes.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://datasette-csvs.glitch.me/"&gt;https://datasette-csvs.glitch.me/&lt;/a&gt;  (&lt;a href="https://glitch.com/edit/#!/datasette-csvs"&gt;view source&lt;/a&gt;) shows the results, running against a simple &lt;code&gt;example.csv&lt;/code&gt; file I created.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Remixing_98"&gt;&lt;/a&gt;Remixing!&lt;/h3&gt;
&lt;p&gt;Here’s where things get really fun: Glitch projects support “remixing”, whereby anyone can click a link to create their own editable copy of a project.&lt;/p&gt;
&lt;p&gt;Remixing works even if you aren’t logged in to Glitch! Anonymous projects expire after five days, so be sure to sign in with GitHub or Facebook if you want to keep yours around.&lt;/p&gt;
&lt;p&gt;Try it out now: Visit &lt;a href="https://glitch.com/edit/#!/remix/datasette-csvs"&gt;https://glitch.com/edit/#!/remix/datasette-csvs&lt;/a&gt; to create your own remix of my project. Then drag a new CSV file directly into the editor and within a few seconds Datasette on Glitch will be up and running against a converted copy of your file!&lt;/p&gt;
&lt;h3&gt;&lt;a id="Limitations_106"&gt;&lt;/a&gt;Limitations&lt;/h3&gt;
&lt;p&gt;The Glitch help center article &lt;a href="https://glitch.com/help/restrictions/"&gt;What technical restrictions are in place?&lt;/a&gt; describes their limits. Most importantly, projects are limited to 4,000 requests an hour - and there’s currently no way to increase that limit. They also limit projects to 200MB of disk space - easily enough to get started exploring some interesting CSV files with Datasette.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Next_steps_110"&gt;&lt;/a&gt;Next steps&lt;/h3&gt;
&lt;p&gt;I’m delighted at how easy this was to setup, and how much power the ability to remix these Datasette demos provides. I’m tempted to start creating remixable Glitch demos that illustrate other aspects of Datasette’s functionality such as &lt;a href="https://datasette.readthedocs.io/en/stable/plugins.html"&gt;plugins&lt;/a&gt; or &lt;a href="https://datasette.readthedocs.io/en/stable/full_text_search.html"&gt;full-text search&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Glitch is an exceptionally cool piece of software. I look forward to seeing their Python support continue to evolve.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/glitch"&gt;glitch&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/hosting"&gt;hosting&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="glitch"/><category term="hosting"/><category term="projects"/><category term="datasette"/></entry><entry><title>Quoting Glitch</title><link href="https://simonwillison.net/2010/Feb/10/javascript/#atom-tag" rel="alternate"/><published>2010-02-10T11:40:44+00:00</published><updated>2010-02-10T11:40:44+00:00</updated><id>https://simonwillison.net/2010/Feb/10/javascript/#atom-tag</id><summary type="html">
    &lt;blockquote cite="http://glitch.com/"&gt;&lt;p&gt;Glitch is built in an entirely new and different way for a game. The back end (java at the lowest level, with game logic scripted in Javascript) is designed for maximum flexibility and ease of deployment. That means we'll be able to push new content — new items, new places, new characters — on a daily basis. It also means that we'll have lots of APIs with which the game can be expanded and extended.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="http://glitch.com/"&gt;Glitch&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/glitch"&gt;glitch&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/java"&gt;java&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rhino"&gt;rhino&lt;/a&gt;&lt;/p&gt;



</summary><category term="glitch"/><category term="java"/><category term="javascript"/><category term="rhino"/></entry><entry><title>glitch zen</title><link href="https://simonwillison.net/2010/Feb/10/glitch/#atom-tag" rel="alternate"/><published>2010-02-10T11:36:14+00:00</published><updated>2010-02-10T11:36:14+00:00</updated><id>https://simonwillison.net/2010/Feb/10/glitch/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://glitchzen.com/"&gt;glitch zen&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Glitch is the upcoming online game from Tiny Speck, many of whom are ex Flickr and indeed ex Game Never Ending before that. Glitch Zen is the first fan site.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/flickr"&gt;flickr&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/glitch"&gt;glitch&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/glitchzen"&gt;glitchzen&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gne"&gt;gne&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tinyspeck"&gt;tinyspeck&lt;/a&gt;&lt;/p&gt;



</summary><category term="flickr"/><category term="glitch"/><category term="glitchzen"/><category term="gne"/><category term="tinyspeck"/></entry></feed>