<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: ben-welsh</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/ben-welsh.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2024-12-03T20:26:49+00:00</updated><author><name>Simon Willison</name></author><entry><title>Quoting Ben Welsh</title><link href="https://simonwillison.net/2024/Dec/3/ben-welsh/" rel="alternate"/><published>2024-12-03T20:26:49+00:00</published><updated>2024-12-03T20:26:49+00:00</updated><id>https://simonwillison.net/2024/Dec/3/ben-welsh/</id><summary type="html">
    &lt;blockquote cite="https://observablehq.com/blog/ben-welsh"&gt;&lt;p&gt;Open source is really part of my process of getting unstuck, learning and contributing back to the community, and also helping future me have an easier time. ‘Me’ is probably the number one beneficiary of my open-source software work. To be honest with you, a lot of it is selfish. It's really about making me more productive, happier, and less stressed. For people who wonder why we should do open source, I think that they should consider that they themselves may benefit more than they realize.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://observablehq.com/blog/ben-welsh"&gt;Ben Welsh&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ben-welsh"&gt;ben-welsh&lt;/a&gt;&lt;/p&gt;



</summary><category term="open-source"/><category term="ben-welsh"/></entry><entry><title>storysniffer</title><link href="https://simonwillison.net/2022/Aug/1/storysniffer/" rel="alternate"/><published>2022-08-01T23:40:13+00:00</published><updated>2022-08-01T23:40:13+00:00</updated><id>https://simonwillison.net/2022/Aug/1/storysniffer/</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://palewi.re/docs/storysniffer/"&gt;storysniffer&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Ben Welsh built a small Python library that guesses if a URL points to an article on a news website, or if it’s more likely to be a category page or /about page or similar. I really like this as an example of what you can do with a tiny machine learning model: the model is bundled as a ~3MB pickle file as part of the package, and the repository includes the Jupyter notebook that was used to train it.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/machine-learning"&gt;machine-learning&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jupyter"&gt;jupyter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ben-welsh"&gt;ben-welsh&lt;/a&gt;&lt;/p&gt;



</summary><category term="machine-learning"/><category term="python"/><category term="jupyter"/><category term="ben-welsh"/></entry><entry><title>How to push tagged Docker releases to Google Artifact Registry with a GitHub Action</title><link href="https://simonwillison.net/2022/Apr/18/how-to-push-tagged-docker-releases-to-google-artifact-registry-w/" rel="alternate"/><published>2022-04-18T03:41:33+00:00</published><updated>2022-04-18T03:41:33+00:00</updated><id>https://simonwillison.net/2022/Apr/18/how-to-push-tagged-docker-releases-to-google-artifact-registry-w/</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://biglocalnews.org/posts/2022/01/27/docker-google-artifact-action.html"&gt;How to push tagged Docker releases to Google Artifact Registry with a GitHub Action&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Ben Welsh’s writeup includes detailed step-by-step instructions for getting the mysterious “Workload Identity Federation” mechanism to work with GitHub Actions and Google Cloud. I’ve been dragging my heels on figuring this out for quite a while, so it’s great to see the steps described at this level of detail.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google"&gt;google&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/docker"&gt;docker&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ben-welsh"&gt;ben-welsh&lt;/a&gt;&lt;/p&gt;



</summary><category term="github"/><category term="google"/><category term="docker"/><category term="ben-welsh"/></entry><entry><title>Pillar Point Stewards, pypi-to-sqlite, improvements to shot-scraper and appreciating datasette-dashboards</title><link href="https://simonwillison.net/2022/Apr/8/weeknotes/" rel="alternate"/><published>2022-04-08T20:26:36+00:00</published><updated>2022-04-08T20:26:36+00:00</updated><id>https://simonwillison.net/2022/Apr/8/weeknotes/</id><summary type="html">
    &lt;p&gt;This week I helped Natalie launch the Pillar Point Stewards website and built a new tool for loading PyPI package data into SQLite, in order to help promote the excellent datasette-dashboards plugin by Romain Clement.&lt;/p&gt;
&lt;h4 id="pillar-point-stewards"&gt;Pillar Point Stewards&lt;/h4&gt;
&lt;p&gt;I've been helping my wife Natalie Downe build the website for the &lt;a href="https://www.pillarpointstewards.com/"&gt;Pillar Point Stewards&lt;/a&gt; initative that she is organizing on behalf of the San Mateo MPA Collaborative and California Academy of Sciences.&lt;/p&gt;
&lt;p&gt;We live in El Granada, CA - home to the Pillar Point reef.&lt;/p&gt;
&lt;p&gt;The reef has always been mixed-use, with harvesting of sea life such as mussels and clams legal provided the harvesters have an inexpensive fishing license.&lt;/p&gt;
&lt;p&gt;Unfortunately, during the pandemic the number of people harvesting the reef raised by an order of magnitude - up to over a thousand people in just a single weekend. This had a major impact on the biodiversity of the reef, as described in &lt;a href="https://baynature.org/2021/01/12/packed-at-pillar-point/"&gt;Packed at Pillar Point by Anne Marshall-Chalmers for Bay Nature&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Pillar Point Stewards is an initiative to recruit volunteer stewards to go out on the reef during low tides, talking to people and trying to inspire curiosity and discourage unsustainable harvesting practices.&lt;/p&gt;
&lt;p&gt;A very small part of the project is the website to support it, which helps volunteers sign up for shifts at low tides.&lt;/p&gt;
&lt;p&gt;We re-used some of the work we had previously done &lt;a href="https://simonwillison.net/2020/Aug/21/weeknotes-rocky-beaches/"&gt;for Rocky Beaches&lt;/a&gt;, in particular the logic for working with tide times &lt;a href="https://tidesandcurrents.noaa.gov/web_services_info.html"&gt;from NOAA&lt;/a&gt; to decide when the shifts should be.&lt;/p&gt;
&lt;p&gt;Natalie designed the site and built the front-end. I implemented the Django backend and integrated with &lt;a href="https://auth0.com/"&gt;Auth0&lt;/a&gt; in order to avoid running our own signup and registration flow. This was the inspiration for the &lt;a href="https://datasette.io/plugins/datasette-auth0"&gt;datasette-auth0&lt;/a&gt; plugin I &lt;a href="https://simonwillison.net/2022/Mar/28/datasette-auth0/"&gt;released last week&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Signed in volunteers can select their shift times from a calendar:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/www-pillarpointstewards-com.jpg" alt="The signed in homepage, showing a list of upcoming shifts and a calendar view." style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;We also included an SVG tide chart on each shift page using the tide data from NOAA, which looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/www-pillarpointstewards-com-shifts-182.png" alt="The tide chart shows the tide level throughout the day, highlighting the low tide and showing which portion of the day is covered by the shift" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;We've been building the site in public. You can see how everything works in the &lt;a href="https://github.com/natbat/pillarpointstewards"&gt;natbat/pillarpointstewards&lt;/a&gt; GitHub repository, including how the site uses &lt;a href="https://github.com/natbat/pillarpointstewards/blob/165cdcfe8b87cb15742e2729d0077202102fc751/.github/workflows/test.yml#L39-L45"&gt;continuous deployment&lt;/a&gt; against &lt;a href="https://fly.io/"&gt;Fly&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="datasette-dashboards"&gt;datasette-dashboards&lt;/h4&gt;
&lt;p&gt;This is not my project, but I'm writing about it here because I only just found out about it and it's really cool.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://romain-clement.net/"&gt;Romain Clement&lt;/a&gt; built a plugin for Datasette called &lt;a href="https://github.com/rclement/datasette-dashboards"&gt;datasette-dashboards&lt;/a&gt;. It's best explained by checking out his &lt;a href="https://datasette-dashboards-demo.vercel.app/-/dashboards/job-offers-stats"&gt;live demo&lt;/a&gt;, which looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/datasette-dashboards-with-filters.png" alt="A dashboard, showing Job offers statistics - with a line chart, a big number, a donut chart, a nested bar chart and a choropleth map. The elements are arranged in a visually pleasing grid, with the line chart taking up two columns while everything else takes up one." style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;There are a bunch of clever ideas in this plugin.&lt;/p&gt;
&lt;p&gt;It uses YAML syntax to define the different dashboard panels, outsourcing the actual visualization elements to Vega. You can see &lt;a href="https://github.com/rclement/datasette-dashboards/blob/202fb2fcbc9efe4848fe940fae435eff75bb4f59/demo/metadata.yml"&gt;the YAML for the demo here&lt;/a&gt;. Here's an edited subset of the YAML illustrating some interesting points:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;&lt;span class="pl-ent"&gt;plugins&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;datasette-dashboards&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;job-offers-stats&lt;/span&gt;:
      &lt;span class="pl-ent"&gt;title&lt;/span&gt;: &lt;span class="pl-s"&gt;Job offers statistics&lt;/span&gt;
      &lt;span class="pl-ent"&gt;description&lt;/span&gt;: &lt;span class="pl-s"&gt;Gather metrics about job offers&lt;/span&gt;
      &lt;span class="pl-ent"&gt;layout&lt;/span&gt;:
        - &lt;span class="pl-s"&gt;[analysis-note, offers-day, offers-day, offers-count]&lt;/span&gt;
        - &lt;span class="pl-s"&gt;[analysis-note, offers-source, offers-day-source, offers-region]&lt;/span&gt;
      &lt;span class="pl-ent"&gt;filters&lt;/span&gt;:
        &lt;span class="pl-ent"&gt;date_start&lt;/span&gt;:
          &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Date Start&lt;/span&gt;
          &lt;span class="pl-ent"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;date&lt;/span&gt;
          &lt;span class="pl-ent"&gt;default&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;2021-01-01&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
        &lt;span class="pl-ent"&gt;date_end&lt;/span&gt;:
          &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Date End&lt;/span&gt;
          &lt;span class="pl-ent"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;date&lt;/span&gt;
      &lt;span class="pl-ent"&gt;charts&lt;/span&gt;:
        &lt;span class="pl-ent"&gt;analysis-note&lt;/span&gt;:
          &lt;span class="pl-ent"&gt;library&lt;/span&gt;: &lt;span class="pl-s"&gt;markdown&lt;/span&gt;
          &lt;span class="pl-ent"&gt;display&lt;/span&gt;: &lt;span class="pl-s"&gt;|-&lt;/span&gt;
&lt;span class="pl-s"&gt;            # Analysis details&lt;/span&gt;
&lt;span class="pl-s"&gt;            ...&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;        &lt;span class="pl-ent"&gt;offers-count&lt;/span&gt;:
          &lt;span class="pl-ent"&gt;title&lt;/span&gt;: &lt;span class="pl-s"&gt;Total number of offers&lt;/span&gt;
          &lt;span class="pl-ent"&gt;db&lt;/span&gt;: &lt;span class="pl-s"&gt;jobs&lt;/span&gt;
          &lt;span class="pl-ent"&gt;query&lt;/span&gt;: &lt;span class="pl-s"&gt;SELECT count(*) as count FROM offers_view WHERE TRUE [[ AND date &amp;gt;= date(:date_start) ]] [[ AND date &amp;lt;= date(:date_end) ]];&lt;/span&gt;
          &lt;span class="pl-ent"&gt;library&lt;/span&gt;: &lt;span class="pl-s"&gt;metric&lt;/span&gt;
          &lt;span class="pl-ent"&gt;display&lt;/span&gt;:
            &lt;span class="pl-ent"&gt;field&lt;/span&gt;: &lt;span class="pl-s"&gt;count&lt;/span&gt;
            &lt;span class="pl-ent"&gt;prefix&lt;/span&gt;:
            &lt;span class="pl-ent"&gt;suffix&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt; offers&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
        &lt;span class="pl-ent"&gt;offers-day&lt;/span&gt;:
          &lt;span class="pl-ent"&gt;title&lt;/span&gt;: &lt;span class="pl-s"&gt;Number of offers by day&lt;/span&gt;
          &lt;span class="pl-ent"&gt;db&lt;/span&gt;: &lt;span class="pl-s"&gt;jobs&lt;/span&gt;
          &lt;span class="pl-ent"&gt;query&lt;/span&gt;: &lt;span class="pl-s"&gt;SELECT date(date) as day, count(*) as count FROM offers_view WHERE TRUE [[ AND date &amp;gt;= date(:date_start) ]] [[ AND date &amp;lt;= date(:date_end) ]] GROUP BY day ORDER BY day&lt;/span&gt;
          &lt;span class="pl-ent"&gt;library&lt;/span&gt;: &lt;span class="pl-s"&gt;vega&lt;/span&gt;
          &lt;span class="pl-ent"&gt;display&lt;/span&gt;:
            &lt;span class="pl-ent"&gt;mark&lt;/span&gt;: &lt;span class="pl-s"&gt;{ type: line, tooltip: true }&lt;/span&gt;
            &lt;span class="pl-ent"&gt;encoding&lt;/span&gt;:
              &lt;span class="pl-ent"&gt;x&lt;/span&gt;: &lt;span class="pl-s"&gt;{ field: day, type: temporal }&lt;/span&gt;
              &lt;span class="pl-ent"&gt;y&lt;/span&gt;: &lt;span class="pl-s"&gt;{ field: count, type: quantitative }&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The SQL query for each panel is defined as &lt;code&gt;query:&lt;/code&gt; - and can take parameters such as &lt;code&gt;:date_end&lt;/code&gt; which are defined by the &lt;code&gt;filters:&lt;/code&gt; section. Note that here one of the filters has a type of &lt;code&gt;date&lt;/code&gt;, which turns into a &lt;code&gt;&amp;lt;input type="date"&amp;gt;&lt;/code&gt; in the filter interface.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;library: vega&lt;/code&gt; panels the &lt;code&gt;display:&lt;/code&gt; key holds the raw &lt;a href="https://vega.github.io/vega/docs/specification/"&gt;Vega specification&lt;/a&gt;, so anything the Vega visualization library can do is available to the plugin.&lt;/p&gt;
&lt;p&gt;I didn't know Vega could render choropleth maps! That map there is defined by this YAML, which loads a GeoJSON file of the regions in France from the &lt;a href="https://github.com/gregoiredavid/france-geojson"&gt;gregoiredavid/france-geojson&lt;/a&gt; GitHub repository.&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;&lt;span class="pl-ent"&gt;display&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;mark&lt;/span&gt;: &lt;span class="pl-s"&gt;geoshape&lt;/span&gt;
  &lt;span class="pl-ent"&gt;projection&lt;/span&gt;: &lt;span class="pl-s"&gt;{ type: mercator }&lt;/span&gt;
  &lt;span class="pl-ent"&gt;transform&lt;/span&gt;:
    - &lt;span class="pl-ent"&gt;lookup&lt;/span&gt;: &lt;span class="pl-s"&gt;region&lt;/span&gt;
      &lt;span class="pl-ent"&gt;from&lt;/span&gt;:
        &lt;span class="pl-ent"&gt;data&lt;/span&gt;:
          &lt;span class="pl-ent"&gt;url&lt;/span&gt;: &lt;span class="pl-s"&gt;https://raw.githubusercontent.com/gregoiredavid/france-geojson/master/regions.geojson&lt;/span&gt;
          &lt;span class="pl-ent"&gt;format&lt;/span&gt;: &lt;span class="pl-s"&gt;{ type: json, property: features }&lt;/span&gt;
        &lt;span class="pl-ent"&gt;key&lt;/span&gt;: &lt;span class="pl-s"&gt;properties.nom&lt;/span&gt;
        &lt;span class="pl-ent"&gt;fields&lt;/span&gt;: &lt;span class="pl-s"&gt;[type, geometry]&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I think my favourite trick though is the way it handles layout. The layout for the demo is defined thus:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;&lt;span class="pl-ent"&gt;layout&lt;/span&gt;:
  - &lt;span class="pl-s"&gt;[analysis-note, offers-day, offers-day, offers-count]&lt;/span&gt;
  - &lt;span class="pl-s"&gt;[analysis-note, offers-source, offers-day-source, offers-region]&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is then implemented &lt;a href="https://github.com/rclement/datasette-dashboards/blob/202fb2fcbc9efe4848fe940fae435eff75bb4f59/datasette_dashboards/templates/dashboard_view.html#L8-L26"&gt;using CSS grids&lt;/a&gt;! Here's the template fragment that does the work:&lt;/p&gt;
&lt;div class="highlight highlight-text-html-django"&gt;&lt;pre&gt;&amp;lt;&lt;span class="pl-ent"&gt;style&lt;/span&gt;&amp;gt;&lt;span class="pl-s1"&gt;&lt;/span&gt;
&lt;span class="pl-s1"&gt;  &lt;span class="pl-k"&gt;@media&lt;/span&gt; (&lt;span class="pl-c1"&gt;min-width&lt;/span&gt;: &lt;span class="pl-c1"&gt;800&lt;span class="pl-k"&gt;px&lt;/span&gt;&lt;/span&gt;) {&lt;/span&gt;
&lt;span class="pl-s1"&gt;    &lt;span class="pl-e"&gt;.dashboard-grid&lt;/span&gt; {&lt;/span&gt;
&lt;span class="pl-s1"&gt;      {% &lt;span class="pl-c1"&gt;if&lt;/span&gt; &lt;span class="pl-c1"&gt;dashboard&lt;/span&gt;.&lt;span class="pl-c1"&gt;layout&lt;/span&gt; %}&lt;/span&gt;
&lt;span class="pl-s1"&gt;      &lt;span class="pl-ent"&gt;grid-template-areas&lt;/span&gt;: {% &lt;span class="pl-c1"&gt;for&lt;/span&gt; &lt;span class="pl-c1"&gt;row&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-c1"&gt;dashboard&lt;/span&gt;.&lt;span class="pl-c1"&gt;layout&lt;/span&gt; %}"{% &lt;span class="pl-c1"&gt;for&lt;/span&gt; &lt;span class="pl-c1"&gt;col&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-c1"&gt;row&lt;/span&gt; %}{{ &lt;span class="pl-c1"&gt;col&lt;/span&gt; }} {% &lt;span class="pl-c1"&gt;endfor&lt;/span&gt; %}" {% &lt;span class="pl-c1"&gt;endfor&lt;/span&gt; %};&lt;/span&gt;
&lt;span class="pl-s1"&gt;      {% &lt;span class="pl-c1"&gt;else&lt;/span&gt; %}&lt;/span&gt;
&lt;span class="pl-s1"&gt;      &lt;span class="pl-ent"&gt;grid-template-columns&lt;/span&gt;: repeat(2, 1fr);&lt;/span&gt;
&lt;span class="pl-s1"&gt;      {% &lt;span class="pl-c1"&gt;endif&lt;/span&gt; %}&lt;/span&gt;
&lt;span class="pl-s1"&gt;    }&lt;/span&gt;
&lt;span class="pl-s1"&gt;&lt;/span&gt;
&lt;span class="pl-s1"&gt;    {% &lt;span class="pl-c1"&gt;if&lt;/span&gt; &lt;span class="pl-c1"&gt;dashboard&lt;/span&gt;.&lt;span class="pl-c1"&gt;layout&lt;/span&gt; %}&lt;/span&gt;
&lt;span class="pl-s1"&gt;    {% &lt;span class="pl-c1"&gt;for&lt;/span&gt; &lt;span class="pl-c1"&gt;chart&lt;/span&gt;_&lt;span class="pl-c1"&gt;slug&lt;/span&gt;, &lt;span class="pl-c1"&gt;chart&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-c1"&gt;dashboard&lt;/span&gt;.&lt;span class="pl-c1"&gt;charts&lt;/span&gt;.&lt;span class="pl-c1"&gt;items&lt;/span&gt;() %}&lt;/span&gt;
&lt;span class="pl-s1"&gt;    &lt;span class="pl-e"&gt;#card-&lt;/span&gt;{{ &lt;span class="pl-c1"&gt;chart&lt;/span&gt;_&lt;span class="pl-c1"&gt;slug&lt;/span&gt; }} {&lt;/span&gt;
&lt;span class="pl-s1"&gt;      &lt;span class="pl-c1"&gt;&lt;span class="pl-c1"&gt;grid-area&lt;/span&gt;&lt;/span&gt;: {{ chart_slug }};&lt;/span&gt;
&lt;span class="pl-s1"&gt;    }&lt;/span&gt;
&lt;span class="pl-s1"&gt;    {% &lt;span class="pl-c1"&gt;endfor&lt;/span&gt; %}&lt;/span&gt;
&lt;span class="pl-s1"&gt;    {% &lt;span class="pl-c1"&gt;endif&lt;/span&gt; %}&lt;/span&gt;
&lt;span class="pl-s1"&gt;  }&lt;/span&gt;
&lt;span class="pl-s1"&gt;&lt;/span&gt;&amp;lt;/&lt;span class="pl-ent"&gt;style&lt;/span&gt;&amp;gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Such a clever and elegant trick.&lt;/p&gt;
&lt;h4 id="pypi-to-sqlite"&gt;pypi-to-sqlite&lt;/h4&gt;
&lt;p&gt;I wanted to add &lt;code&gt;datasette-dashboards&lt;/code&gt; to the official Datasette &lt;a href="https://datasette.io/plugins"&gt;plugins directory&lt;/a&gt;, but there was a catch: since most of the plugins listed there are written by me, the site has some baked in expectations: in particular, it expects that plugins will all be using the GitHub releases feature (&lt;a href="https://github.com/simonw/datasette-graphql/releases"&gt;for example&lt;/a&gt;) to announce their releases.&lt;/p&gt;
&lt;p&gt;Romain's plugin wasn't using that feature, instead maintaining &lt;a href="https://github.com/rclement/datasette-dashboards/blob/master/CHANGELOG.md"&gt;its own changelog file&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I've been meaning to make the plugin directory more forgiving for a while. I decided to switch from using GitHub releases as the definitive source of release information to using releases published to &lt;a href="https://pypi.org/"&gt;PyPI (the Python package index)&lt;/a&gt; instead.&lt;/p&gt;
&lt;p&gt;PyPI offers a stable JSON API: &lt;a href="https://pypi.org/pypi/datasette-dashboards/json"&gt;https://pypi.org/pypi/datasette-dashboards/json&lt;/a&gt; - which includes information on the package and all of its releases.&lt;/p&gt;
&lt;p&gt;To reliably pull that information into &lt;a href="https://datasette.io"&gt;datasette.io&lt;/a&gt; I decided on a two-step process. First, I set up a &lt;a href="https://simonwillison.net/2020/Oct/9/git-scraping/"&gt;Git scraper&lt;/a&gt; to archive the data that I cared about into a new repository called &lt;a href="https://github.com/simonw/pypi-datasette-packages"&gt;pypi-datasette-packages&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That repo stores the current PyPI JSON for every package listed on the Datasette website. This means I can see changes made to those files over time by browsing the commit history. It also means that if PyPI is unavailable I can still &lt;a href="https://simonwillison.net/2020/Dec/13/datasette-io/"&gt;build and deploy&lt;/a&gt; the site.&lt;/p&gt;
&lt;p&gt;Then I wrote a new tool called &lt;a href="https://datasette.io/tools/pypi-to-sqlite"&gt;pypi-to-sqlite&lt;/a&gt; to load that data into SQLite database tables. You can try that out like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pypi-to-sqlite
pypi-to-sqlite pypi.db datasette-dashboards pypi-to-sqlite --prefix pypi_
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;--prefix&lt;/code&gt; option causes the tables to be created with the specified prefix in their names.&lt;/p&gt;
&lt;p&gt;Here are the three tables generated by that command:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://datasette.io/content/pypi_packages"&gt;pypi_packages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datasette.io/content/pypi_versions"&gt;pypi_versions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datasette.io/content/pypi_releases"&gt;pypi_releases&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using data from these tables I was able to &lt;a href="https://github.com/simonw/datasette.io/issues/98#issuecomment-1093144133"&gt;rework the SQL view&lt;/a&gt; that powers the plugins and tools directories on the site, and now &lt;a href="https://datasette.io/plugins/datasette-dashboards"&gt;datasette-dashboards has its own page there&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="shot-scraper-10-11"&gt;shot-scraper 0.10 and 0.11&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/shot-scraper"&gt;shot-scraper&lt;/a&gt; is my tool for taking automated screenshots of web pages, built on top of &lt;a href="https://playwright.dev/"&gt;Playwright&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://twitter.com/palewire"&gt;Ben Welsh&lt;/a&gt; has been a key early adopter of &lt;code&gt;shot-scraper&lt;/code&gt;, using it to power his &lt;a href="https://github.com/palewire/news-homepages"&gt;news-homepages&lt;/a&gt; project which takes screenshots of various news websites and then both &lt;a href="https://twitter.com/newshomepages"&gt;tweets the results&lt;/a&gt; and uploads them to the &lt;a href="https://archive.org/details/news-homepages"&gt;News Homepages collection&lt;/a&gt; on the Internet Archive.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/shot-scraper/releases/tag/0.10"&gt;shot-scraper 0.10&lt;/a&gt; is mostly Ben's work: he contributed both a &lt;code&gt;--timeout&lt;/code&gt; option and a &lt;code&gt;--browser&lt;/code&gt; option to let you install and use browsers other than the Chromium default!&lt;/p&gt;
&lt;p&gt;(Ben needed this because some news homepages were embedding videos in a format that &lt;a href="https://github.com/microsoft/playwright/issues/13093"&gt;wasn't supported by Chromium&lt;/a&gt; but did work fine in regular Chrome.)&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/ryancheley"&gt;Ryan Cheley&lt;/a&gt; also contributed to 0.10 - thanks to Ryan, the &lt;code&gt;shot-scraper multi&lt;/code&gt; command now continues taking shots even if one of them fails, unless you pass the &lt;code&gt;--fail-on-error&lt;/code&gt; flag.&lt;/p&gt;
&lt;p&gt;In writing my weeknotes, I decided to use &lt;code&gt;shot-scraper&lt;/code&gt; to take a screenshot of the signed in homepage of the &lt;a href="https://www.pillarpointstewards.com/"&gt;www.pillarpointstewards.com&lt;/a&gt; site.&lt;/p&gt;
&lt;p&gt;In doing so, I found out that Google SSO &lt;a href="https://github.com/simonw/shot-scraper/issues/61"&gt;refuses to work&lt;/a&gt; with the default Playwright Chromium! But it does continue to work with Firefox, so I fixed the &lt;code&gt;shot-scraper auth&lt;/code&gt; to support the &lt;code&gt;--browser&lt;/code&gt; option.&lt;/p&gt;
&lt;p&gt;I took the screenshot like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shot-scraper auth https://www.pillarpointstewards.com/ -b firefox auth.json
# Now manually sign in with Auth0 and Google
shot-scraper https://www.pillarpointstewards.com/ -b firefox -a auth.json \
  --javascript "
    Array.from(
      document.querySelectorAll('[href^=tel]')
    ).forEach(el =&amp;gt; el.innerHTML = '(xxx) xxx-xxxx')"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;--javascript&lt;/code&gt; line there redacts the phone numbers that are displayed on the page to signed in volunteers.&lt;/p&gt;
&lt;p&gt;I created the second screenshot of just the tide times chart using this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shot-scraper https://www.pillarpointstewards.com/shifts/182/ \
  -b firefox -a auth.json \
  --selector '.primary h2:nth-child(8)' \
  --selector .day-alone --padding 15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/shot-scraper/releases/tag/0.11"&gt;shot-scraper 0.11&lt;/a&gt;, released a few minutes ago, contains the new &lt;code&gt;auth --browser&lt;/code&gt; feature plus some additional contributions from Ben Welsh, Ryan Murphy and Ian Wootten:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;New &lt;code&gt;shot-scraper accessibility --timeout&lt;/code&gt; option, thanks &lt;a href="https://github.com/palewire"&gt;Ben Welsh&lt;/a&gt;. &lt;a href="https://github.com/simonw/shot-scraper/pull/59"&gt;#59&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shot-scraper auth --browser&lt;/code&gt; option for authentication using a browser other than Chromium. &lt;a href="https://github.com/simonw/shot-scraper/issues/61"&gt;#61&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;--quality&lt;/code&gt; now results in a JPEG file with the correct &lt;code&gt;.jpg&lt;/code&gt; extension. Thanks, &lt;a href="https://github.com/iwootten"&gt;Ian Wootten&lt;/a&gt;. &lt;a href="https://github.com/simonw/shot-scraper/pull/58"&gt;#58&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;New &lt;code&gt;--reduced-motion&lt;/code&gt; flag for emulating the "prefers-reduced-motion" media feature. Thanks, &lt;a href="https://github.com/rdmurphy"&gt;Ryan Murphy&lt;/a&gt;. &lt;a href="https://github.com/simonw/shot-scraper/issues/49"&gt;#49&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/shot-scraper"&gt;shot-scraper&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/shot-scraper/releases/tag/0.11"&gt;0.11&lt;/a&gt; - (&lt;a href="https://github.com/simonw/shot-scraper/releases"&gt;12 releases total&lt;/a&gt;) - 2022-04-08
&lt;br /&gt;Tools for taking automated screenshots of websites&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/pypi-to-sqlite"&gt;pypi-to-sqlite&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/pypi-to-sqlite/releases/tag/0.2.2"&gt;0.2.2&lt;/a&gt; - (&lt;a href="https://github.com/simonw/pypi-to-sqlite/releases"&gt;3 releases total&lt;/a&gt;) - 2022-04-08
&lt;br /&gt;Load data about Python packages from PyPI into SQLite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/shot-scraper"&gt;shot-scraper&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/shot-scraper/releases/tag/0.10"&gt;0.10&lt;/a&gt; - (&lt;a href="https://github.com/simonw/shot-scraper/releases"&gt;12 releases total&lt;/a&gt;) - 2022-03-29
&lt;br /&gt;Tools for taking automated screenshots of websites&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/webassembly/compile-to-wasm-llvm-macos"&gt;Compiling to WASM with llvm on macOS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/python/calendar-weeks"&gt;Generating a calendar week grid with the Python Calendar module&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/docker/docker-for-mac-container-to-postgresql-on-host"&gt;Allowing a container in Docker Desktop for Mac to talk to a PostgreSQL server on the host machine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/pytest/treat-warnings-as-errors"&gt;Treating warnings as errors in pytest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/auth0/auth0-logout"&gt;Logging users out of Auth0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/bash/use-awk-to-add-a-prefix"&gt;Using awk to add a prefix&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/continuous-deployment"&gt;continuous-deployment&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/continuous-integration"&gt;continuous-integration&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/natalie-downe"&gt;natalie-downe&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pypi"&gt;pypi&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/shot-scraper"&gt;shot-scraper&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ben-welsh"&gt;ben-welsh&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="continuous-deployment"/><category term="continuous-integration"/><category term="natalie-downe"/><category term="plugins"/><category term="projects"/><category term="pypi"/><category term="datasette"/><category term="weeknotes"/><category term="shot-scraper"/><category term="ben-welsh"/></entry><entry><title>@newshomepages</title><link href="https://simonwillison.net/2022/Mar/12/newshomepages/" rel="alternate"/><published>2022-03-12T19:21:34+00:00</published><updated>2022-03-12T19:21:34+00:00</updated><id>https://simonwillison.net/2022/Mar/12/newshomepages/</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://twitter.com/newshomepages"&gt;@newshomepages&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Ben Welsh used my shot-scraper tool and GitHub Actions to launch a Twitter bot which tweets screenshots of newspaper homepages on a scheduled basis. Ben says: “The tech is so easy, I was able to pull it off in a couple hours at zero cost. A decade ago I ran a similar project using the cloud resources of the day. [...] It costs thousands of dollars and the screenshots were of much lower quality. Incredible progress!”

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/twitter"&gt;twitter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/playwright"&gt;playwright&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/shot-scraper"&gt;shot-scraper&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ben-welsh"&gt;ben-welsh&lt;/a&gt;&lt;/p&gt;



</summary><category term="twitter"/><category term="github-actions"/><category term="playwright"/><category term="shot-scraper"/><category term="ben-welsh"/></entry><entry><title>Weeknotes: Covid-19, First Python Notebook, more Dogsheep, Tailscale</title><link href="https://simonwillison.net/2020/Apr/1/weeknotes/" rel="alternate"/><published>2020-04-01T20:29:59+00:00</published><updated>2020-04-01T20:29:59+00:00</updated><id>https://simonwillison.net/2020/Apr/1/weeknotes/</id><summary type="html">
    &lt;p&gt;My &lt;a href="https://covid-19.datasettes.com/"&gt;covid-19.datasettes.com&lt;/a&gt; project publishes information on COVID-19 cases around the world. The project started out using data &lt;a href="https://github.com/CSSEGISandData/COVID-19"&gt;from Johns Hopkins CSSE&lt;/a&gt;, but last week the New York Times &lt;a href="https://www.nytimes.com/article/coronavirus-county-data-us.html"&gt;started publishing&lt;/a&gt; high quality USA county- and state-level daily numbers to their &lt;a href="https://github.com/nytimes/covid-19-data"&gt;own repository&lt;/a&gt;. Here's &lt;a href="https://github.com/simonw/covid-19-datasette/commit/56e1644390e5d01ff67c61d6c165749093675632"&gt;the change&lt;/a&gt; that added the NY Times data.&lt;/p&gt;

&lt;p&gt;It's very easy to use this data to accidentally build misleading things. I've been &lt;a href="https://github.com/simonw/covid-19-datasette/blob/master/README.md"&gt;updating the README&lt;/a&gt; with links about this - my current favourite is &lt;a href="https://fivethirtyeight.com/features/why-its-so-freaking-hard-to-make-a-good-covid-19-model/"&gt;Why It’s So Freaking Hard To Make A Good COVID-19 Model&lt;/a&gt; by  Maggie Koerth, Laura Bronner and Jasmine Mithani at FiveThirtyEight.&lt;/p&gt;

&lt;h3 id="weeknotes-first-python-notebook"&gt;First Python Notebook&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://twitter.com/palewire"&gt;Ben Welsh&lt;/a&gt; from the LA Times teaches a course called &lt;a href="https://www.firstpythonnotebook.org/"&gt;First Python Notebook&lt;/a&gt; at journalism conferences such as NICAR. He ran a free online version the course last weekend, and I offered to help out as a TA.&lt;/p&gt;

&lt;p&gt;Most of the help I provided came before the course: Ben asked attendees to confirm that they had working installations of Python 3 and pipenv, and if they didn't volunteers such as myself would step in to help. I had Zoom and email conversations with at least ten people to help them get their environments into shape.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://xkcd.com/1987/"&gt;This XKCD&lt;/a&gt; neatly summarizes the problem:&lt;/p&gt;

&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2020/python_environment_2x.png" alt="XKCD Python Environments" style="max-width: 100%" /&gt;&lt;/p&gt;

&lt;p&gt;One of the most common problems I had to debug was PATH issues: people had installed the software, but due to various environmental differences &lt;code&gt;python3&lt;/code&gt; and &lt;code&gt;pipenv&lt;/code&gt; weren't available on the PATH. Talking people through the obscurities of creating a &lt;code&gt;~/.bashrc&lt;/code&gt; file and using it to define a PATH over-ride really helps emphasize how arcane this kind of knowledge is.&lt;/p&gt;

&lt;p&gt;I enjoyed this comment:&lt;/p&gt;

&lt;blockquote&gt;&lt;p&gt;"Welcome to intro to Tennis. In the first two weeks, we'll discuss how to rig a net and resurface a court." - &lt;a href="https://twitter.com/ClausWilke/status/1234941405883138048"&gt;Claus Wilke&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;

&lt;p&gt;Ben's course itself is hands down the best introduction to Python from a Data Journalism perspective I have ever seen. Within an hour of starting the students are using Pandas in a Jupyter notebook to find interesting discrepancies in California campaign finance data.&lt;/p&gt;

&lt;p&gt;If you want to check it out yourself, the entire four hour workshop &lt;a href="https://twitter.com/palewire/status/1244410903279177728"&gt;is now on YouTube&lt;/a&gt; and closely follows the material on &lt;a href="https://www.firstpythonnotebook.org/"&gt;firstpythonnotebook.org&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id="weeknotes-coronavirus-diary"&gt;Coronavirus Diary&lt;/h3&gt;

&lt;p&gt;We are clearly living through a notable and very painful period of history right now. On the 19th of March (just under two weeks ago, but time is moving both really fast and incredibly slowly right now) I started a personal diary - something I've never done before. It lives in an Apple Note and I'm adding around a dozen paragraphs to it every day. I think it's helping. I'm sure it will be interesting to look back on in a few years time.&lt;/p&gt;

&lt;h3 id="weeknotes-dogsheep"&gt;Dogsheep&lt;/h3&gt;

&lt;p&gt;Much of my development work this past week has gone into my &lt;a href="https://github.com/dogsheep"&gt;Dogsheep&lt;/a&gt; suite of tools for personal analytics.&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;I upgraded the entire family of tools for compatibility with &lt;a href="https://sqlite-utils.readthedocs.io/en/stable/changelog.html#v2"&gt;sqlite-utils 2.x&lt;/a&gt;.&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/dogsheep/pocket-to-sqlite"&gt;pocket-to-sqlite&lt;/a&gt; got a major upgrade: it now fetches items using Pocket's API pagination (previously it just tried to pull in 5,000 items in one go) and has the ability to only fetch new items. As a result I'm now running it from cron in my personal Dogsheep instance, so "Save to Pocket" is now my preferred Dogsheep-compatible way of bookmarking content.&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/dogsheep/twitter-to-sqlite"&gt;twitter-to-sqlite&lt;/a&gt; got a couple of important new features in &lt;a href="https://github.com/dogsheep/twitter-to-sqlite/releases/tag/0.20"&gt;release 0.20&lt;/a&gt;. I fixed &lt;a href="https://github.com/dogsheep/twitter-to-sqlite/issues/39"&gt;a nasty bug&lt;/a&gt; in the &lt;code&gt;--since&lt;/code&gt; flag where retweets from other accounts could cause new tweets from an account to be ignored. I also added a new &lt;code&gt;count_history&lt;/code&gt; table which automatically tracks changes to a Twitter user's friends, follower and listed counts over time (&lt;a href="https://github.com/dogsheep/twitter-to-sqlite/issues/40"&gt;#40&lt;/a&gt;).&lt;/li&gt;&lt;/ul&gt;

&lt;p&gt;I'm also now using Dogsheep for some journalism! I'm working with the &lt;a href="https://biglocalnews.org/"&gt;Big Local News&lt;/a&gt; team at Stanford to help track and archive tweets by a number of different US politicians and health departments relating to the ongoing pandemic. This collaboration resulted in the above improvements to &lt;code&gt;twitter-to-sqlite&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id="weeknotes-tailscale"&gt;Tailscale&lt;/h3&gt;

&lt;p&gt;My personal Dogsheep is currently protected by &lt;a href="https://simonwillison.net/2019/Oct/5/client-side-certificate-authentication-nginx/"&gt;client certificates&lt;/a&gt;, so only my personal laptop and iPhone (with the right certificates installed) can connect to the web server it is running on.&lt;/p&gt;

&lt;p&gt;I spent a bit of time this week playing with &lt;a href="https://tailscale.com/"&gt;Tailscale&lt;/a&gt;, and I'm &lt;em&gt;really&lt;/em&gt; impressed by it.&lt;/p&gt;

&lt;p&gt;Tailscale is a commercial company built on top of &lt;a href="https://www.wireguard.com/"&gt;WireGuard&lt;/a&gt;, the new approach to VPN tunnels which just &lt;a href="https://arstechnica.com/gadgets/2020/03/wireguard-vpn-makes-it-to-1-0-0-and-into-the-next-linux-kernel/"&gt;got merged&lt;/a&gt; into the Linux 5.6 kernel. Tailscale first caught my attention in January when they &lt;a href="https://bradfitz.com/2020/01/30/joining-tailscale"&gt;hired Brad Fitzpatrick&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;WireGuard lets you form a private network by having individual hosts exchange public/private keys with each other. Tailscale provides software which manages those keys for you, making it trivial to set up a private network between different nodes.&lt;/p&gt;

&lt;p&gt;How trivial? It took me less than ten minutes to get a three-node private network running between my iPhone, laptop and a Linux server. I installed the &lt;a href="https://apps.apple.com/us/app/tailscale/id1470499037?ls=1"&gt;iPhone app&lt;/a&gt;, the &lt;a href="https://tailscale.com/kb/1037/install-ubuntu-1804"&gt;Ubuntu package&lt;/a&gt;, the &lt;a href="https://apps.apple.com/ca/app/tailscale/id1475387142?mt=12"&gt;OS X app&lt;/a&gt;, signed them all into my Google account and I was done.&lt;/p&gt;

&lt;p&gt;Each of those devices now has an additional IP address in the 100.x range which they can use to talk to each other. Tailscale guarantees that the IP address will stay constant for each of them.&lt;/p&gt;

&lt;p&gt;Since the network is public/private key encrypted between the nodes, Tailscale can't see any of my traffic - they're purely acting as a key management mechanism. And it's free: Tailscale charge for networks with multiple users, but a personal network like this is free of charge.&lt;/p&gt;

&lt;p&gt;I'm not running my own personal Dogsheep on it yet, but I'm tempted to switch over. I'd love other people to start running their own personal Dogsheep instances but I'm paranoid about encouraging this when securing them is so important. Tailscale looks like it might be a great solution for making secure personal infrastructure more easily and widely available.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/brad-fitzpatrick"&gt;brad-fitzpatrick&lt;/a&gt;, &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/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/teaching"&gt;teaching&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dogsheep"&gt;dogsheep&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tailscale"&gt;tailscale&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/covid19"&gt;covid19&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ben-welsh"&gt;ben-welsh&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="brad-fitzpatrick"/><category term="data-journalism"/><category term="projects"/><category term="python"/><category term="teaching"/><category term="datasette"/><category term="dogsheep"/><category term="weeknotes"/><category term="tailscale"/><category term="covid19"/><category term="ben-welsh"/></entry><entry><title>Los Angeles Weedmaps analysis</title><link href="https://simonwillison.net/2019/May/30/los-angeles-weedmaps-analysis/" rel="alternate"/><published>2019-05-30T04:35:42+00:00</published><updated>2019-05-30T04:35:42+00:00</updated><id>https://simonwillison.net/2019/May/30/los-angeles-weedmaps-analysis/</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://nbviewer.jupyter.org/github/datadesk/la-weedmaps-analysis/blob/master/notebook.ipynb"&gt;Los Angeles Weedmaps analysis&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Ben Welsh at the LA Times published this Jupyter notebook showing the full working behind a story they published about LA’s black market weed dispensaries. I picked up several useful tricks from it—including how to load points into a geopandas GeoDataFrame (in epsg:4326 aka WGS 84) and how to then join that against the LA Times neighborhoods GeoJSON boundaries file.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/palewire/status/1133723284116086784"&gt;Ben Welsh&lt;/a&gt;&lt;/small&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/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/latimes"&gt;latimes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pandas"&gt;pandas&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jupyter"&gt;jupyter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ben-welsh"&gt;ben-welsh&lt;/a&gt;&lt;/p&gt;



</summary><category term="data-journalism"/><category term="geospatial"/><category term="latimes"/><category term="pandas"/><category term="jupyter"/><category term="ben-welsh"/></entry><entry><title>Helicopter accident analysis notebook</title><link href="https://simonwillison.net/2018/Nov/19/helicopter-accident-analysis/" rel="alternate"/><published>2018-11-19T18:25:07+00:00</published><updated>2018-11-19T18:25:07+00:00</updated><id>https://simonwillison.net/2018/Nov/19/helicopter-accident-analysis/</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://nbviewer.jupyter.org/github/datadesk/helicopter-accident-analysis/blob/master/notebook.ipynb"&gt;Helicopter accident analysis notebook&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Ben Welsh worked on an article for the LA Times about helicopter accident rates, and has published the underlying analysis as an extremely detailed Jupyter notebook. Lots of neat new (to me) notebook tricks in here as well.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/palewire/status/1064189967289659392"&gt;Ben Welsh&lt;/a&gt;&lt;/small&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/jupyter"&gt;jupyter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ben-welsh"&gt;ben-welsh&lt;/a&gt;&lt;/p&gt;



</summary><category term="data-journalism"/><category term="jupyter"/><category term="ben-welsh"/></entry><entry><title>Django Bakery</title><link href="https://simonwillison.net/2018/Jun/16/django-bakery/" rel="alternate"/><published>2018-06-16T01:49:35+00:00</published><updated>2018-06-16T01:49:35+00:00</updated><id>https://simonwillison.net/2018/Jun/16/django-bakery/</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://django-bakery.readthedocs.io/en/latest/"&gt;Django Bakery&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
“A set of helpers for baking your Django site out as flat files”. Released by the LA Times Data Desk, who use it for a large number of projects from election results to data journalism interactives. Statically publishing these projects to S3 lets them handle huge traffic spikes at a very low cost.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/palewire/status/1007721981179883520"&gt;@palewire&lt;/a&gt;&lt;/small&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/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/s3"&gt;s3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/static-generator"&gt;static-generator&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ben-welsh"&gt;ben-welsh&lt;/a&gt;&lt;/p&gt;



</summary><category term="data-journalism"/><category term="django"/><category term="s3"/><category term="static-generator"/><category term="ben-welsh"/></entry></feed>