<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: digitalocean</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/digitalocean.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2020-10-07T02:52:54+00:00</updated><author><name>Simon Willison</name></author><entry><title>Running Datasette on DigitalOcean App Platform</title><link href="https://simonwillison.net/2020/Oct/7/datasette-digitalocean-app-platform/#atom-tag" rel="alternate"/><published>2020-10-07T02:52:54+00:00</published><updated>2020-10-07T02:52:54+00:00</updated><id>https://simonwillison.net/2020/Oct/7/datasette-digitalocean-app-platform/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://til.simonwillison.net/til/til/digitalocean_datasette-on-digitalocean-app-platform.md"&gt;Running Datasette on DigitalOcean App Platform&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I spent some time with DigitalOcean’s new App Platform today, which is a Heroku-style PaaS that starts at $5/month. It looks like it could be a really good fit for Datasette. Disk is ephemeral, but if you’re publishing read-only data that doesn’t matter since you can build the SQLite database as part of the deployment and bundle it up in the Docker/Kubernetes container.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/deployment"&gt;deployment&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/digitalocean"&gt;digitalocean&lt;/a&gt;&lt;/p&gt;



</summary><category term="deployment"/><category term="heroku"/><category term="datasette"/><category term="digitalocean"/></entry><entry><title>Weeknotes: Datasette Cloud and zero downtime deployments</title><link href="https://simonwillison.net/2020/Jan/21/weeknotes-datasette-cloud-and-zero-downtime-deployments/#atom-tag" rel="alternate"/><published>2020-01-21T20:56:46+00:00</published><updated>2020-01-21T20:56:46+00:00</updated><id>https://simonwillison.net/2020/Jan/21/weeknotes-datasette-cloud-and-zero-downtime-deployments/#atom-tag</id><summary type="html">
    &lt;p&gt;Yesterday's piece on &lt;a href="https://simonwillison.net/2020/Jan/21/github-actions-cloud-run/"&gt;Tracking FARA by deploying a data API using GitHub Actions and Cloud Run&lt;/a&gt; was originally intended to be my weeknotes, but ended up getting a bit too involved.&lt;/p&gt;

&lt;p&gt;Aside from playing with GitHub Actions and Cloud Run, my focus over the past week has been working on Datasette Cloud. Datasette Cloud is the current name I'm using for my hosted &lt;a href="https://datasette.readthedocs.io/"&gt;Datasette&lt;/a&gt; product - the idea being that I'll find it &lt;em&gt;a lot&lt;/em&gt; easier to get &lt;a href="https://simonwillison.net/2019/Sep/10/jsk-fellowship/"&gt;feedback on Datasette from journalists&lt;/a&gt; if they can use it without having to install anything!&lt;/p&gt;

&lt;p&gt;My MVP for Datasette Cloud is that I can use it to instantly provision a new, private Datasette instance for a journalist (or team of journalists) that they can then sign into, start playing with and start uploading their data to (initially as CSV files).&lt;/p&gt;

&lt;p&gt;I have to solve quite a few problems to get there:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;Secure, isolated instances of Datasette. A team or user should only be able to see their own files. I plan to solve this using Docker containers that are mounted such that they can only see their own dedicated volumes.&lt;/li&gt;&lt;li&gt;The ability to provision new instances as easily as possible - and give each one its own HTTPS subdomain.&lt;/li&gt;&lt;li&gt;Authentication: users need to be able to register and sign in to accounts. I could use &lt;a href="https://github.com/simonw/datasette-auth-github"&gt;datasette-auth-github&lt;/a&gt; for this but I'd like to be able to support regular email/password accounts too.&lt;/li&gt;&lt;li&gt;Users need to be able to upload CSV files and have them converted into a SQLite database compatible with Datasette.&lt;/li&gt;&lt;/ul&gt;

&lt;h3&gt;Zero downtime deployments&lt;/h3&gt;

&lt;p&gt;I have a stretch goal which I'm taking pretty seriously: I want to have a mechanism in place for zero-downtime deployments of new versions of the software.&lt;/p&gt;

&lt;p&gt;Arguable this is an unneccessary complication for an MVP. I may not fully implement it, but I do want to at least know that the path I've taken is compatible with zero downtime deployments.&lt;/p&gt;

&lt;p&gt;Why do zero downtime deployments matter so much to me? Because they are desirable for rapid iteration, and crucial for setting up continuious deployment. Even a couple of seconds of downtime during a deployment leaves a psychological desire not to deploy too often. I've seen the productivity boost that deploying fearlessly multiple times a day brings, and I want it.&lt;/p&gt;

&lt;p&gt;So I've been doing a bunch of research into zero downtime deployment options (thanks to some &lt;a href="https://twitter.com/simonw/status/1217599189921628160"&gt;great help on Twitter&lt;/a&gt;) and I think I have something that's going to work for me.&lt;/p&gt;

&lt;p&gt;The first ingredient is &lt;a href="https://docs.traefik.io/"&gt;Traefik&lt;/a&gt; - a new-to-me edge router (similar to nginx) which has a delightful focus on runtime configuration based on automatic discovery.&lt;/p&gt;

&lt;p&gt;It works with a bunch of different technology stacks, but I'm going to be using it with regular Docker. Traefik watches for new Docker containers, reads their labels and uses that to reroute traffic to them.&lt;/p&gt;

&lt;p&gt;So I can launch a new Docker container, apply the Docker label &lt;code&gt;"traefik.frontend.rule": "Host:subdomain.mydomain.com"&lt;/code&gt; and Traefik will start proxying traffic to that subdomain directly to that container.&lt;/p&gt;

&lt;p&gt;Traefik also has extremely robust built-in support for Lets Encrypt to issue certificates. I managed to &lt;a href="https://docs.traefik.io/https/acme/#wildcard-domains"&gt;issue a wildcard TLS certificate&lt;/a&gt; for my entire domain, so new subdomains are encrypted straight away. This did require me to give Traefik API access to modify DNS entries - I'm running DNS for this project on Digital Ocean and thankfully Traefik knows how to do this by talking to their API.&lt;/p&gt;

&lt;p&gt;That solves provisioning: when I create a new account I can call the Docker API (from Python) to start up a new, labelled container on a subdomain protected by a TLS certificate.&lt;/p&gt;

&lt;p&gt;I still needed a way to run a zero-downtime deployment of a new container (for example when I release a new version of Datasette and want to upgrade everyone). After quite a bit of research (during which I discovered you can't modify the labels on a Docker container without restarting it) I settled on the approach described in &lt;a href="https://coderbook.com/@marcus/how-to-do-zero-downtime-deployments-of-docker-containers/"&gt;this article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Essentially you configure Traefik to retry failed requests, start a new, updated container with the same routing information as the existing one (causing Traefik to load balance HTTP requests across both), then shut down the old container and trust Traefik to retry in-flight requests against the one that's still running.&lt;/p&gt;

&lt;p&gt;Rudimentary testing with &lt;code&gt;ab&lt;/code&gt; suggested that this is working as desired.&lt;/p&gt;

&lt;p&gt;One remaining problem: if Traefik is running in a Docker container and proxying all of my traffic, how can I upgrade Traefik itself without any downtime?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/simonw/status/1218604019033100288"&gt;Consensus on Twitter&lt;/a&gt; seems to be that Docker on its own doesn't have a great mechanism for this (I was hoping I could re-route port 80 traffic to the host to a different container in an atomic way). But... &lt;code&gt;iptables&lt;/code&gt; has mechanisms that can re-route traffic from one port to another - so I should be able to run a new Traefik container on a different port and re-route to it at the operating system level.&lt;/p&gt;

&lt;p&gt;That's quite enough yak shaving around zero time deployments for now!&lt;/p&gt;

&lt;h3 id="datasette-upload-csvs"&gt;datasette-upload-csvs&lt;/h3&gt;

&lt;p&gt;A big problem I'm seeing with the current Datasette ecosystem is that while Datasette offers a web-based user interface for querying and accessing data, the &lt;a href="https://datasette.readthedocs.io/en/0.33/ecosystem.html#tools-for-creating-sqlite-databases"&gt;tools I've written for actually creating those databases&lt;/a&gt; are decidedly command-line only.&lt;/p&gt;

&lt;p&gt;Telling journalists they have to learn to install and run software on the command-line is way too high a barrier to entry.&lt;/p&gt;

&lt;p&gt;I've always intended to have Datasette plugins that can handle uploading and converting data. It's time to actually build one!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/simonw/datasette-upload-csvs"&gt;datasette-upload-csvs&lt;/a&gt; is what I've got so far. It has a big warning not to use it in the README - it's &lt;em&gt;very&lt;/em&gt; alpha sofware at the moment - but it does prove that the concept can work.&lt;/p&gt;

&lt;p&gt;It uses the &lt;a href="https://datasette.readthedocs.io/en/stable/plugins.html#asgi-wrapper-datasette"&gt;asgi_wrapper&lt;/a&gt; plugin hook to intercept requests to the path &lt;code&gt;/-/upload-csv&lt;/code&gt; and forward them on to another ASGI app, written using Starlette, which provides a basic upload form and then handles the upload.&lt;/p&gt;

&lt;p&gt;Uploaded CSVs are converted to SQLite using &lt;a href="https://sqlite-utils.readthedocs.io/"&gt;sqlite-utils&lt;/a&gt; and written to the first mutable database attached to Datasette.&lt;/p&gt;

&lt;p&gt;It needs a bunch more work (and tests) before I'm comfortable telling people to use it, but it does at least exist as a proof of concept for me to iterate on.&lt;/p&gt;

&lt;h3&gt;datasette-auth-django-cookies&lt;/h3&gt;

&lt;p&gt;No code for this yet, but I'm beginning to flesh it out as a concept.&lt;/p&gt;

&lt;p&gt;I don't particularly want to implement user registration and authentication and cookies and password hashing. I know how to do it, which means I know it's not something you shuld re-roll for every project.&lt;/p&gt;

&lt;p&gt;Django has a really well designed, robust authentication system. Can't I just use that?&lt;/p&gt;

&lt;p&gt;Since all of my applications will be running on subdomains of a single domain, my current plan is to have a regular Django application which handles registration and logins. Each subdomain will then run a custom piece of Datasette ASGI middleware which knows how to read and validate the Django authentication cookie.&lt;/p&gt;

&lt;p&gt;This should give me single sign-on with a single, audited codebase for registration and login with (hopefully) the least amount of work needed to integrate it with Datasette.&lt;/p&gt;

&lt;p&gt;Code for this will hopefully follow over the next week.&lt;/p&gt;

&lt;h3&gt;Niche Museums - now publishing weekly&lt;/h3&gt;

&lt;p&gt;I hit a milestone with my &lt;a href="https://www.niche-museums.com/"&gt;Niche Museums&lt;/a&gt; project: the site now lists details of 100 museums!&lt;/p&gt;

&lt;p&gt;For the 100th entry I decided to celebrate with by far the most rewarding (and exclusive) niche museum experience I've ever had: &lt;a href="https://www.niche-museums.com/browse/museums/100"&gt;Ray Bandar's Bone Palace&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You should read the entry. The short version is that Ray Bandar collected 7,000 animals skulls over a sixty year period, and Natalie managed to score us a tour of his incredible basement mere weeks before the collection was donated to the California Academy of Sciences.&lt;/p&gt;

&lt;img src="https://niche-museums.imgix.net/ray-bandar.jpeg?w=1600&amp;amp;h=800&amp;amp;fit=crop&amp;amp;auto=compress" alt="The basement full of skulls" style="max-width: 100%" /&gt;

&lt;p&gt;Posting one museum a day was taking increasingly more of my time, as I had to delve into the depths of my museums-I-have-visited backlog and do increasing amounts of research. Now that I've hit 100 I'm going to switch to publishing one a week, which should also help me visit new ones quickly enough to keep the backlog full!&lt;/p&gt;

&lt;p&gt;So I only posted four this week:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/97"&gt;The ruins of Llano del Rio&lt;/a&gt; in Los Angeles County&lt;/li&gt;&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/98"&gt;Cleveland Hungarian Museum&lt;/a&gt; in Cleveland&lt;/li&gt;&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/99"&gt;New Orleans Historic Voodoo Museum&lt;/a&gt; in New Orleans&lt;/li&gt;&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/100"&gt;Ray Bandar's Bone Palace&lt;/a&gt; in San Francisco&lt;/li&gt;&lt;/ul&gt;

&lt;p&gt;I also &lt;a href="https://github.com/simonw/museums/commits/842dfb96"&gt;built a simple JavaScript image gallery&lt;/a&gt; to better display the 54 photos I published from our trip to Ray Bandar's basement.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/csv"&gt;csv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/deployment"&gt;deployment&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/museums"&gt;museums&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zero-downtime"&gt;zero-downtime&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/docker"&gt;docker&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/traefik"&gt;traefik&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-cloud"&gt;datasette-cloud&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/digitalocean"&gt;digitalocean&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="csv"/><category term="deployment"/><category term="museums"/><category term="projects"/><category term="zero-downtime"/><category term="docker"/><category term="datasette"/><category term="weeknotes"/><category term="traefik"/><category term="datasette-cloud"/><category term="digitalocean"/></entry><entry><title>Weeknotes: Improv at Stanford, planning Datasette Cloud</title><link href="https://simonwillison.net/2020/Jan/14/stanford-planning-datasette-cloud/#atom-tag" rel="alternate"/><published>2020-01-14T00:22:18+00:00</published><updated>2020-01-14T00:22:18+00:00</updated><id>https://simonwillison.net/2020/Jan/14/stanford-planning-datasette-cloud/#atom-tag</id><summary type="html">
    &lt;p&gt;Last week was the first week of the quarter at Stanford - which is called "shopping week" here because students are expected to try different classes to see which ones they are going to stick with.&lt;/p&gt;

&lt;p&gt;I've settled on three classes this quarter: &lt;a href="https://explorecourses.stanford.edu/search?q=taps+103"&gt;Beginning Improvising&lt;/a&gt;, &lt;a href="https://explorecourses.stanford.edu/search?q=DESINST%20240"&gt;Designing Machine Learning&lt;/a&gt; and &lt;a href="https://explorecourses.stanford.edu/search?q=STRAMGT%20353"&gt;Entrepreneurship: Formation of New Ventures&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Beginning Improvising&lt;/strong&gt; is the Stanford improv theater course. It's a big time commitment: three two-hours sessions a week for ten weeks is nearly 60 hours of improv!&lt;/p&gt;

&lt;p&gt;It's already proving to be really interesting though: it turns out the course is a thinly disguised applied psychology course.&lt;/p&gt;

&lt;p&gt;Improv is about creating a creative space for other people to shine. The applications to professional teamwork are obvious and fascinating to me. I'll probably write more about this as the course continues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Designing Machine Learning&lt;/strong&gt; is a class at the Stanford d.School taught by &lt;a href="https://twitter.com/michellercarney"&gt;Michelle Carney&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/emilykathryn/"&gt;Emily Callaghan&lt;/a&gt;. It focuses on multidisciplinary applications of machine learning, mixing together students from many different disciplines around Stanford.&lt;/p&gt;

&lt;p&gt;I took a &lt;a href="https://simonwillison.net/2018/Oct/29/transfer-learning/"&gt;fast.ai deep learning course&lt;/a&gt; last year which gave me a basic understanding of the code size of neural networks, but I'm much more interestind in figuring out applications so this seems like a much more interesting option than a more code-focused course.&lt;/p&gt;

&lt;p&gt;The class started out building some initial models using &lt;a href="https://teachablemachine.withgoogle.com/"&gt;Google's Teachable Machine tool&lt;/a&gt;, which is &lt;em&gt;fascinating&lt;/em&gt;. It lets you train transfer learning models for image, audio and posture recognition entirely in your browser - no data is transferred to Google's servers at all. You can then export those models and use them with a variety of different libraries - I've got them to work with both JavaScript and Python already.&lt;/p&gt;

&lt;p&gt;I'm taking &lt;strong&gt;Entrepreneurship: Formation of New Ventures&lt;/strong&gt; because of the rave reviews I heard from other JSK fellows who took it last quarter. It's a classic case-study business school class: each session features a guest speaker who is a successful entrepreneur, and the class discusses their case for the first two thirds of the section while they listen in - then finds out how well the discussion matched to what actually happened.&lt;/p&gt;

&lt;h3&gt;Planning Datasette Cloud&lt;/h3&gt;

&lt;p&gt;Shopping week kept me pretty busy so I've not done much actual development over the past week, but I have started planning out and researching my next major project, which I'm currently calling &lt;em&gt;Datasette Cloud&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Datasette Cloud will be an invite-only hosted SaaS version of &lt;a href="https://datasette.readthedocs.io/"&gt;Datasette&lt;/a&gt;. It's designed to help get news organizations on board with the software without having to talk them through figuring out their own hosting, so I can help them solve real problems and learn more about how the ecosystem should evolve to support them.&lt;/p&gt;

&lt;p&gt;I'd love to be able to run this on serverless hosting platforms like Google Cloud Run or Heroku, but sadly those tools aren't an option to me due to a key problem: I'm trying to build a &lt;em&gt;stateful&lt;/em&gt; service (SQLite databases need to live on a local disk) in 2020.&lt;/p&gt;

&lt;p&gt;I posed this challenge &lt;a href="https://twitter.com/simonw/status/1182077259839991808"&gt;on Twitter&lt;/a&gt; back in October:&lt;/p&gt;

&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;What&amp;#39;s the easiest way of running a stateful web application these days?&lt;br /&gt;&lt;br /&gt;Stateful as in it supports a process which can accept web requests and is allowed to write to a durable disk&lt;br /&gt;&lt;br /&gt;So not Heroku/Zeit Now/Cloud Run etc&lt;/p&gt;- Simon Willison (@simonw) &lt;a href="https://twitter.com/simonw/status/1182077259839991808?ref_src=twsrc%5Etfw"&gt;October 9, 2019&lt;/a&gt;&lt;/blockquote&gt;

&lt;p&gt;I've been exploring my options since then, and I think I've settled on a decidedly 2010-era way of doing this: I'm going to run my own instances! So I've been exploring hosting Datasette on both AWS Lightsail and Digital Ocean Droplets over the past few months.&lt;/p&gt;

&lt;p&gt;My current plan is to have each Datasette Cloud account run as a Datasette instance in its own Docker container, primarily to ensure filesystem isolation: different accounts must not be able to see each other's database files.&lt;/p&gt;

&lt;p&gt;I started &lt;a href="https://twitter.com/simonw/status/1216468790508015616"&gt;another discussion about this&lt;/a&gt; on Twitter and had several recommendations for &lt;a href="https://docs.traefik.io/"&gt;Traefik&lt;/a&gt; as a load balancer for assigning hostnames to different Docker containers, which is exactly what I need to do.&lt;/p&gt;

&lt;p&gt;So this afternoon I made my way through Digital Ocean's outstanding tutorial &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-use-traefik-as-a-reverse-proxy-for-docker-containers-on-ubuntu-18-04"&gt;How To Use Traefik as a Reverse Proxy for Docker Containers on Ubuntu 18.04&lt;/a&gt; and I think I've convinced myself that this is a smart way forward.&lt;/p&gt;

&lt;p&gt;So, mostly a research week but I've got a solid plan for my next steps.&lt;/p&gt;

&lt;h3&gt;This week's Niche Museums&lt;/h3&gt;

&lt;ul&gt;&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/90"&gt;Jelly Belly Factory&lt;/a&gt; in Fairfield, CA&lt;/li&gt;&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/91"&gt;Bevolo Gas Light Museum&lt;/a&gt; in New Orleans, LA&lt;/li&gt;&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/92"&gt;Museo de las Misiones de Baja California&lt;/a&gt; in Loreto&lt;/li&gt;&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/93"&gt;Fort Point&lt;/a&gt; in San Francisco, CA&lt;/li&gt;&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/94"&gt;Donner Memorial State Park Visitor Center&lt;/a&gt; in Nevada County, CA&lt;/li&gt;&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/95"&gt;Anja Community Reserve&lt;/a&gt; in Madagascar&lt;/li&gt;&lt;li&gt;&lt;a href="https://www.niche-museums.com/browse/museums/96"&gt;Palace of Fine Arts&lt;/a&gt; in San Francisco, CA&lt;/li&gt;&lt;/ul&gt;

&lt;p&gt;I also finally got around to &lt;a href="https://www.niche-museums.com/map"&gt;implementing a map&lt;/a&gt;.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/stanford"&gt;stanford&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/docker"&gt;docker&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jsk"&gt;jsk&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-cloud"&gt;datasette-cloud&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/digitalocean"&gt;digitalocean&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="stanford"/><category term="docker"/><category term="jsk"/><category term="weeknotes"/><category term="datasette-cloud"/><category term="digitalocean"/></entry><entry><title>Tracking PG&amp;E outages by scraping to a git repo</title><link href="https://simonwillison.net/2019/Oct/10/pge-outages/#atom-tag" rel="alternate"/><published>2019-10-10T23:32:14+00:00</published><updated>2019-10-10T23:32:14+00:00</updated><id>https://simonwillison.net/2019/Oct/10/pge-outages/#atom-tag</id><summary type="html">
    &lt;p&gt;PG&amp;amp;E have &lt;a href="https://twitter.com/bedwardstiek/status/1182047040932470784"&gt;cut off power&lt;/a&gt; to several million people in northern California, supposedly as a precaution against wildfires.&lt;/p&gt;

&lt;p&gt;As it happens, I've been scraping and recording PG&amp;amp;E's outage data every 10 minutes for the past 4+ months. This data got really interesting over the past two days!&lt;/p&gt;

&lt;p&gt;The original data lives in &lt;a href="https://github.com/simonw/pge-outages"&gt;a GitHub repo&lt;/a&gt; (more importantly in &lt;a href="https://github.com/simonw/pge-outages/commits/master"&gt;the commit history&lt;/a&gt; of that repo).&lt;/p&gt;

&lt;p&gt;Reading JSON in a Git repo isn't particularly productive, so this afternoon I figured out how to transform that data into a SQLite database and publish it with &lt;a href="https://github.com/simonw/datasette"&gt;Datasette&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The result is &lt;code&gt;https://pge-outages.simonwillison.net/&lt;/code&gt; (no longer available)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update from 27th October 2019&lt;/strong&gt;: I also used the data to create this animation (first shared &lt;a href="https://twitter.com/simonw/status/1188612004572880896"&gt;on Twitter&lt;/a&gt;):&lt;/p&gt;

&lt;video style="max-width: 100%" src="https://static.simonwillison.net/static/2019/outages.mp4" controls="controls"&gt;
  Your browser does not support the video tag.
&lt;/video&gt;

&lt;h3 id="thedatamodeloutagesandsnapshots"&gt;The data model: outages and snapshots&lt;/h3&gt;

&lt;p&gt;The three key tables to understand are &lt;code&gt;outages&lt;/code&gt;, &lt;code&gt;snapshots&lt;/code&gt; and &lt;code&gt;outage_snapshots&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;PG&amp;amp;E assign an outage ID to every outage - where an outage is usually something that affects a few dozen customers. I store these in the &lt;a href="https://pge-outages.simonwillison.net/pge-outages/outages?_sort_desc=outageStartTime"&gt;outages table&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Every 10 minutes I grab a snapshot of their full JSON file, which reports every single outage that is currently ongoing. I store a record of when I grabbed that snapshot in the &lt;a href="https://pge-outages.simonwillison.net/pge-outages/snapshots?_sort_desc=id"&gt;snapshots table&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The most interesting table is &lt;code&gt;outage_snapshots&lt;/code&gt;. Every time I see an outage in the JSON feed, I record a new copy of its data as an &lt;code&gt;outage_snapshot&lt;/code&gt; row. This allows me to reconstruct the full history of any outage, in 10 minute increments.&lt;/p&gt;

&lt;p&gt;Here are &lt;a href="https://pge-outages.simonwillison.net/pge-outages/outage_snapshots?snapshot=1269"&gt;all of the outages&lt;/a&gt; that were represented in &lt;a href="https://pge-outages.simonwillison.net/pge-outages/snapshots/1269"&gt;snapshot 1269&lt;/a&gt; - captured at 4:10pm Pacific Time today.&lt;/p&gt;

&lt;p&gt;I can run &lt;code&gt;select sum(estCustAffected) from outage_snapshots where snapshot = 1269&lt;/code&gt; (&lt;a href="https://pge-outages.simonwillison.net/pge-outages?sql=select+sum%28estCustAffected%29+from+outage_snapshots+where+snapshot+%3D+%3Aid&amp;amp;id=1269"&gt;try it here&lt;/a&gt;) to count up the total PG&amp;amp;E estimate of the number of affected customers - it's 545,706!&lt;/p&gt;

&lt;p&gt;I've installed &lt;a href="https://github.com/simonw/datasette-vega"&gt;datasette-vega&lt;/a&gt; which means I can render graphs. Here's my first attempt at a graph showing &lt;a href="https://pge-outages.simonwillison.net/pge-outages?sql=select+snapshots.id%2C+title+as+snapshotTime%2C+hash%2C+sum%28outage_snapshots.estCustAffected%29+as+totalEstCustAffected%0D%0Afrom+snapshots+join+outage_snapshots+on+snapshots.id+%3D+outage_snapshots.snapshot%0D%0Agroup+by+snapshots.id+order+by+snapshots.id+desc+limit+150#g.mark=line&amp;amp;g.x_column=snapshotTime&amp;amp;g.x_type=ordinal&amp;amp;g.y_column=totalEstCustAffected&amp;amp;g.y_type=quantitative"&gt;the number of estimated customers affected over time&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://static.simonwillison.net/static/2019/pge-outages-graph.png" style="text-decoration: none; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2019/pge-outages-graph.png" style="max-width: 100%" /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(I don't know why there's a dip towards the end of the graph).&lt;/p&gt;

&lt;p&gt;I also defined &lt;a href="https://pge-outages.simonwillison.net/pge-outages/most_recent_snapshot"&gt;a SQL view&lt;/a&gt; which shows all of the outages from the most recently captured snapshot (usually within the past 10 minutes if the PG&amp;amp;E website hasn't gone down) and renders them using &lt;a href="https://github.com/simonw/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://static.simonwillison.net/static/2019/pge-map.jpg" style="text-decoration: none; border: none;"&gt;&lt;img src="https://static.simonwillison.net/static/2019/pge-map.jpg" style="max-width: 100%" /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3 id="thingstobeawareof"&gt;Things to be aware of&lt;/h3&gt;

&lt;p&gt;There are a huge amount of unanswered questions about this data. I've just been looking at PG&amp;amp;E's JSON and making guesses about what things like &lt;code&gt;estCustAffected&lt;/code&gt; means. Without official documentation we can only guess as to how accurate this data is, or how it should be interpreted.&lt;/p&gt;

&lt;p&gt;Some things to question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What's the quality of this data? Does it reflect accurately on what's actually going on out there?&lt;/li&gt;

&lt;li&gt;What's the exact meaning of the different columns - &lt;code&gt;estCustAffected&lt;/code&gt;, &lt;code&gt;currentEtor&lt;/code&gt;, &lt;code&gt;autoEtor&lt;/code&gt;, &lt;code&gt;hazardFlag&lt;/code&gt; etc?&lt;/li&gt;

&lt;li&gt;Various columns (&lt;code&gt;lastUpdateTime&lt;/code&gt;, &lt;code&gt;currentEtor&lt;/code&gt;, &lt;code&gt;autoEtor&lt;/code&gt;) appear to be integer &lt;a href="https://en.wikipedia.org/wiki/Unix_time"&gt;unix timestamps&lt;/a&gt;. What timezone were they recorded in? Do they include DST etc?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id="howitworks"&gt;How it works&lt;/h3&gt;

&lt;p&gt;I originally wrote the scraper &lt;a href="https://simonwillison.net/2017/Oct/10/fires-in-the-north-bay/"&gt;back in October 2017&lt;/a&gt; during the North Bay fires, and moved it to run on Circle CI based on my work building &lt;a href="https://simonwillison.net/2019/Mar/13/tree-history/"&gt;a commit history of San Francisco's trees&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's pretty simple: every 10 minutes &lt;a href="https://circleci.com/gh/simonw/disaster-scrapers"&gt;a Circle CI job&lt;/a&gt; runs which scrapes &lt;a href="https://apim.pge.com/cocoutage/outages/getOutagesRegions?regionType=city&amp;amp;expand=true"&gt;the JSON feed&lt;/a&gt; that powers the PG&amp;amp;E website's &lt;a href="https://www.pge.com/myhome/outages/outage/index.shtml"&gt;outage map&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The JSON is then committed to my &lt;a href="https://github.com/simonw/pge-outages"&gt;pge-outages GitHub repository&lt;/a&gt;, over-writing the existing &lt;a href="https://github.com/simonw/pge-outages/blob/master/pge-outages.json"&gt;pge-outages.json file&lt;/a&gt;. There's some code that attempts to generate a human-readable commit message, but the historic data itself is saved in the commit history of that single file.&lt;/p&gt;

&lt;h3 id="buildingthedatasette"&gt;Building the Datasette&lt;/h3&gt;

&lt;p&gt;The hardest part of this project was figuring out how to turn a GitHub commit history of changes to a JSON file into a SQLite database for use with Datasette.&lt;/p&gt;

&lt;p&gt;After a bunch of prototyping in a Jupyter notebook, I ended up with the schema described above.&lt;/p&gt;

&lt;p&gt;The code that generates the database can be found in &lt;a href="https://github.com/simonw/pge-outages/blob/master/build_database.py"&gt;build_database.py&lt;/a&gt;. I used &lt;a href="https://gitpython.readthedocs.io/en/stable/"&gt;GitPython&lt;/a&gt; to read data from the git repository and my &lt;a href="https://sqlite-utils.readthedocs.io/en/stable/python-api.html"&gt;sqlite-utils library&lt;/a&gt; to create and update the database.&lt;/p&gt;

&lt;h3 id="deployment"&gt;Deployment&lt;/h3&gt;

&lt;p&gt;Since this is a large database that changes every ten minutes, I couldn't use the usual &lt;a href="https://datasette.readthedocs.io/en/stable/publish.html "&gt;datasette publish&lt;/a&gt; trick of packaging it up and re-deploying it to a serverless host (Cloud Run or Heroku or Zeit Now) every time it updates.&lt;/p&gt;

&lt;p&gt;Instead, I'm running it on a VPS instance. I ended up trying out Digital Ocean for this, after &lt;a href="https://twitter.com/simonw/status/1182077259839991808"&gt;an enjoyable Twitter conversation&lt;/a&gt; about good options for stateful (as opposed to stateless) hosting.&lt;/p&gt;

&lt;h3 id="nextsteps"&gt;Next steps&lt;/h3&gt;

&lt;p&gt;I'm putting this out there and sharing it with the California News Nerd community in the hope that people can find interesting stories in there and help firm up my methodology - or take what I've done and spin up much more interesting forks of it.&lt;/p&gt;

&lt;p&gt;If you build something interesting with this please let me know, via email (swillison is my Gmail) or &lt;a href="https://twitter.com/simonw"&gt;on Twitter&lt;/a&gt;.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/data-journalism"&gt;data-journalism&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scraping"&gt;scraping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/git-scraping"&gt;git-scraping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/digitalocean"&gt;digitalocean&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="data-journalism"/><category term="projects"/><category term="scraping"/><category term="sqlite"/><category term="datasette"/><category term="git-scraping"/><category term="digitalocean"/><category term="sqlite-utils"/></entry></feed>