alexwlchan
Alex Chan’s personal website
PythonDev tools / editors / IDEsTesting / QARustEssays / personal
- Posts
- 727 from 10 Jan 2013 to 26 Oct 2026
- Posting
- weekly (101 in the last year)
- Kind
- personal blog
- Feed
- https://alexwlchan.net/atom.xml
2026 86
- Positive alerts in Prometheus
- Organising my cross-stitch patterns
- Parametrised tests in Rust with named parameters
- Abusing ID3 chapters to turn videos into glanceable podcasts
- What is WS11 1DB?
- Blocking referrers with Caddy
- How to type a Spanish question mark (¿) on a Mac
- Non-overlapping type comparisons and Python type checkers
- Why does t.Setenv panic after t.Parallel?
- Why can’t you combine <code>.tar.gz</code> files with <code>cat</code>?
- Use Path.glob() and Path.rglob() for typed versions of glob.glob()
- How Tailscale tracked down a 16-year-old SQLite bug →
- How Tailscale tracked down a 16-year-old SQLite bugWe had repeated database corruption until we fixed a rare SQLite corruption bug. What does it take to find such an error?SQLite · 13 Aug 2026
- Curious clocks and colourful eyes
- Track which templates are used by Jinja2
- Archeologists distinguish between “sherds” and “shards”
- A single command to test all my changed Go packages
- Preventing line breaks in <code><code></code> elements
- Fixing a bug with byte order marks
- Disable the new message animations in WhatsApp
- A Git hook to prevent committing directly to main
- Finding high-churn folders that bother Backblaze
- Describing all my photos
- Always-on SSH agent forwarding with my Git pushes
- I don’t want to repeat repeat myself
- Rebuilding the computer room
- Managing the caption of a photo with AppleScript (but not PhotoKit)
- What can wonky APIs tell us about the web?
- Goodhart’s and Campbell’s Law are different
- Using the Screen Capture API to record a browser window
- Notes from The Cornishman No. 176 (Spring 2026)
- Using Pytester to test my Playwright fixtures
- Rendering a chat thread in CSS and JavaScript
- GitUp can’t diff text files larger than 8MB
- Format “human-readable” JSON with FracturedJSON
- Waiting for website changes in the browser
- Watching for file changes on macOS
- Testing the width of a page on a mobile device using Playwright
- Using Playwright to test my static sites
- Disable AirPods charging notifications
- Start a Caddy server in a subprocess during a Python session
- Building a basic cache with SQLite
- Filter a list of JSON object based on a list of tags
- HTTP GET requests with the Python standard library
- Auditing my local Python packages
- Quietly quantum-resistant blogging
- The FileExistsError exception exposes a filename attribute
- HOME_GET_ME_HOME is a Citymapper Shortcuts action
- Why can’t Python connect to example.com?
- The red-lined bubble snail
- Useful type hints for Python
- Creating a personalised bin calendar
- Monki Gras 2026 “Prepping Craft”
- How to truncate the middle of long command output
- The selfish case for public libraries
- What’s the main prefix in SQLite queries?
- AirPlay Receiver can interfere with Flask apps
- My randline project is tested by Crater
- The file(1) command can read SQLite databases
- Dreaming of a ten-year computerModern computers are more powerful than I know what to do with, but the norm is still to upgrade every few years. I want my current computer to last a decade, and I don’t think that’s unreasonable.Computers and code · 12 Mar 2026
- Drawing an image with Liquid Glass using SwiftUI Previews
- Gumdrop, a silly app for messing with my webcam
- Road signs in the Soviet union don’t have circular heads
- Get a map of IP addresses for devices in my tailnet
- Create a file atomically in Go
- Setting up golink in my personal tailnet
- Testing date formatting with date-fns-tz and different timezones
- Use SQL triggers to prevent overwriting a value
- The SQLite command line shell will count your unclosed parentheses
- The bare minimum for syncing Git repos
- Place with the same name, but different etymology
- The “strangler” pattern is named after a tree, not an act of violence
- Creating Caddyfiles with Cog
- Use systemctl is-active to determine if a service is running
- Removing a self-hosted runner from GitHub Actions
- My Git config
- Introducing Mosaic, my Python static site generator
- Python f-strings cheat sheet
- Who is Bufo the frog?
- Parody posters for made-up movies
- The Good, the Bad, and the GuttersA step-by-step guide to a movie poster grid that uses CSS Grid, text-wrap balanced titles, and dynamic hover states.CSS · 14 Jan 2026
- Using perceptual distance to create better headers
- The caret anchor (^) matches differently in Python and Ruby
- Where I store my multi-factor recovery codes
- The passwords I actually memorise
- Quick-and-dirty print debugging in Go
2025 136
- My favourite books from 2025Sapphics and spies, butlers and basketball, prophecy and purpose – what I enjoyed reading this year.Books I’ve read · 31 Dec 2025
- Ignore AI upscaled YouTube videos with yt-dlp
- Drawing Truchet tiles in SVG
- Adding a README to S3 buckets with Terraform
- The palm tree that led to Palmyra
- Meeting my younger selfI reviewed 150,000 fragments of my online life, and I was reminded of the friends I found, the mistakes I made, and the growth I gained.Preserving social media · 12 Dec 2025
- Hard problems in social media archivingPreserving social media is easier said than done. What makes it so difficult for institutions to back up the Internet?Preserving social media · 10 Dec 2025
- The Internet forgets, but I don’t want toI don’t trust platforms to preserve my memories, so I built my own scrapbook of social media.Preserving social media · 8 Dec 2025
- When square pixels aren’t square
- Collapsing whitespace in a Liquid template
- The LastModified date of an S3 multipart upload is when the upload started, not when it finished
- Can you take an ox to Oxford?
- Cleaning up messy dates in JSON
- Detecting AV1-encoded videos with Python
- Python has a builtin tool to do rot13 (among other things)
- Why can’t my iPhone play that video?
- Doing my own syntax highlighting (finally)
- How to find the Pygments token type for a short name
- Remove the microsecond precision from a datetime in Python
- Creating a personal wrapper around yt-dlp
- Don’t show Dock icons from apps on another device
- Look at the __annotations__ to learn about the definition of a TypedDict
- Get the avatar URL for an Instagram page
- Using the Tumblr v1 API doesn’t require auth
- How to expire a Tailscale node key faster than the min expiry
- The “MCP” in Archivematica stands for “Master Control Program”
- Use the -v flag to get verbose output from Go tests
- Opening all the files that have been modified in a Git branch
- Seeing the public node key of a Tailscale node
- Get a list of values in a JSON object with jq
- Linking to text fragments with a bookmarklet
- Don’t nest a <Script> in a <Head> with Next.js
- Using Go to write to a file, but only if it doesn’t exist yet
- Resizing images in Rust, now with EXIF orientation support
- The person who runs the Cambridge NTP servers has an excellent email address
- Using vcrpy to test HTTP interactions in PythonHow I record HTTP requests to get fast, reliable, and consistent tests, and the patterns I use in a production codebase.Python · 28 Aug 2025
- Discard a variable in a JavaScript object spread by assigning it to _
- The @ symbol was added to Morse code in 2004
- How to play with SQLite functions without real data
- Repeatedly run flaky Go tests with stress
- How to list the tests that will be run by a go test command
- My preferred options for SmartyPants in Python
- Get the avatar URL for a Bluesky user
- Looking up posts in the Bluesky API
- Create space-saving clones on macOS with Python
- Slipstitch, Queer Craft, and community spaces
- Today was my last day at the Flickr Foundation
- Minifying HTML on my Jekyll website
- Using zipstream to stream new zip files to object storage with boto3
- Using Linode object storage and boto3
- Adding a string to a tarfile in Python
- Disable HTTP Basic Auth for certain pages in Caddy
- Collecting pytest markers from failing tests
- Moving my Glitch apps to my own web server
- You can change the size of tabs on web pages with the tab-size property
- Combine arrows in Mermaid by using an invisible node
- Show a list of checkboxes in a WTForms form
- Group nodes in a Mermaid flowchart by putting them in a subgraph
- Redacting sensitive information from gunicorn access logs
- Some countries have “poison centers” as part of their healthcare service
- Listen for the popstate event to see when the user clicks the “back” button
- Python’s f-strings support = for self-documenting expressions
- Comparing two files in a bash script
- Use typing.getargs() to get a list of typing.Literal[…] values
- Ruby’s range can iterate over more than just numbers
- The error “No aggregated item, sequence was empty” comes from Jinja2
- Get a string representation of a Python traceback with traceback.format_exc()
- How to run a task on a schedule on macOS
- Go’s compiler is smart enough to spot division by zero errors
- Recreating the bird animation from Swift.orgThe Swift.org website got a redesign, including an animation of a swooping bird, and I read the source code to understand how it works.Drawing things · 11 Jun 2025
- Parsing JSON in Go with a required field
- Print a comma-separated number in Python with {num:,}
- Get the duration of a video file with mediainfo
- Use text=true with subprocess functions to get stdout/stderr as str, not bytes
- What I learnt about making websites by reading two thousand web pagesHow to write thoughtful HTML, new-to-me features of CSS, and some quirks and relics I found while building my personal web archive.Web archiving · 26 May 2025
- Building a personal archive of the web, the slow wayHow I built a web archive by hand, the tradeoffs between manual and automated archiving, and what I learnt about preserving the web.Web archiving · 19 May 2025
- The Panamanian Golden Frog communicates by semaphore
- Web archiving
- What does the word “gubernatorial” mean?
- How to stream lines from stdout with subprocess
- You can set/update the total of a progress bar tqdm after it starts
- Creating a static website for all my bookmarksTo help me keep track of interesting links, I created a static website that keeps all my data locally. Why do I care about bookmarks, and how does my new site work?Web archiving · 12 May 2025
- How do I find photos of a person on Flickr?
- Handling JSON objects with duplicate names in Python
- How does Flickr’s getLicenseHistory handle photos with no license changes?
- Where does AirDrop save files on macOS?
- Python 3.14 includes a pi-related Easter egg
- A faster way to copy SQLite databases between computers
- How to check if Tailscale is running
- To filter the results of a SQLite tally for values with a certain frequency, use a HAVING instead of a WHERE clause
- When fixing mojibake, use ftfy.fix_and_explain() to understand how it’s fixing a piece of text
- Why isn’t delete_where() deleting rows in sqlite-utils?
- A flash of light in the darkness
- console.log() holds a reference to an object, not a copy of it
- Beyond None: actionable error messages for keyring.get_password()
- Localising the <time> with JavaScript
- My favourite websites from my bookmark collection
- Always runningWhen the fire starts, I am already running for the exit. I run because somebody must show the way.Art and creativity · 6 Apr 2025
- You can set an output mode for SQLite
- How do you write ratios in the aspect-ratio property?
- Python 3.13 throws a ResourceWarning for SQLite databases that aren’t explicitly closed
- How to install exiftool in GitHub Actions
- What I’ve Learned by Building to LastThere are patterns in what lasts; people skills matter more than technical skills; long-lasting systems cannot grow forever.Digital preservation · 28 Mar 2025
- Whose code am I running in GitHub Actions?
- Fast and random sampling in SQLite
- We all lose when art is anonymised
- An unexpected lesson in CSS stacking contexts
- Creating static map images with OpenStreetMap, Web Mercator, and Pillow
- How to get a macOS file/folder icon in Swift
- It’s cool to careCaring deeply – about art, my friends, and shared experiences – has brought me so much joy.Personal thoughts · 21 Feb 2025
- The British Forces Post Office (BFPO) and the Loamshire Regiment
- Cosmetic updates to this site
- How I create static websites for tiny archivesStart with a simple HTML file, then add features like templates, sorting, and filtering as they become useful.Tiny archives · 19 Feb 2025
- Create an animated placeholder box with CSS
- Disable JavaScript in an <iframe> by setting the sandbox attribute
- Get video dimensions on macOS with built-in tools
- Get the dimensions of a video file with mediainfo
- Downloading avatars from Tumblr
- Good embedded toots
- Unexpected errors in the BagIt areaExploring the many ways a BagIt bag can be invalid, and how you might want to build a process for validating bags to ensure the quality of your digital archive.Digital preservation · 8 Feb 2025
- What does Flickr return in the flickr.photo.getSizes API for videos?
- A basic socket server in Ruby
- Bitly will delete your account if you don’t use it for three years
- Making inventory spreadsheets for my LEGO sets
- Adding auto-generated cover images to EPUBs downloaded from AO3
- How to find the content.opf in an EPUB file
- Getting and setting the cover image from an EPUB file
- Google will delete your account if you don’t use it for two years
- How to find platform-specific directories
- Looking at images in a spreadsheet
- Restricting routes to pre-approved IP addresses in Caddy
- randline: get a random selection of lines in a file using reservoir sampling
- Use shutil.copyfileobj and xb to avoid overwriting files when copying in Python
- How to tally the attributes of the top N rows in a SQLite table
- How I test Rust command-line apps with assert_cmd
- How I use the notes field in my password manager
2024 136
- My favourite books from 2024Fires and fighters, heroes and horrors, suicide and surgery – what I enjoyed reading this year.Books I’ve read · 31 Dec 2024
- I deleted all my tweets
- Not all blog posts are created equalSplitting my site into “articles” and “today I learned” helped me overcome writer’s block and improve my writing.Blogging about blogging · 26 Dec 2024
- How to get the IP address of a device in my Tailnet
- There are limits on the styles you can apply with :visited
- How to find the biggest files backed up by Backblaze
- Editing a filename in Finder will convert it to NFD
- How can I work out what program is keeping a disk open?
- You can reset the start of a regex in Ruby
- How to get the expiry date of an HTTPS certificate with Ruby
- How to install an asset from a GitHub release
- Debugging some confusing behaviour with find and xargs
- Creating a reverse proxy to a multi-site server with Caddy
- How to find all of Apple’s system icons
- How I name my computersWhat do flaming birds, man-eating giants, and obscure deities have in common?Computers and code · 14 Nov 2024
- The Apollo 11 astronauts were promised another attempt if they had to abort
- Drawing a diagonal banner over the corner of an image
- Find the shortest prefix to identify a string in Ruby
- How to get the target of an HTTP redirect with curl
- What is the author_name in the list of tags on a Flickr photo?
- How to get a user’s email address with the Flickr API
- Making an “under construction” element in pure CSS
- HTML strings may not be equivalent if you minify them
- What errors can you get from hyperlink.parse?
- Using static websites for tiny archivesI’ve been creating small, hand-written websites to organise my files. It’s a lightweight, flexible approach that I hope will last a long time.Tiny archives · 16 Oct 2024
- Making alt text more visible
- How do the ispublic, isfriend and isfamily flags work in the Flickr API?
- Moving my website from Netlify to Caddy
- A script to verify my Netlify redirects
- Two examples of hover styles on images
- Use std::io::IsTerminal to detect if you’re running in the terminal in Rust
- Drawing a better bandwidth graph for Netlify
- Going between Finder and the Terminal
- Digital declutteringI’m resisting my temptation towards digital hoarding and “save everything”, and trying to be more selective about the data I’m keeping.Personal thoughts · 13 Sep 2024
- Use $_ to get the last argument to the previous bash command
- With Flask-Login, you want current_user == None, not current_user is None
- How to get the expiry date of an HTTPS certificate with Python
- Mountaintop momentsThe amazing events that let us temporarily escape from our lives, and how to deal with the emotional crash that comes afterward.Personal thoughts · 26 Aug 2024
- create_thumbnail: create smaller versions of imagesI’ve made a new tool that allows me to reuse my thumbnailing code across all my projects.Images and videos · 20 Aug 2024
- Plates and statesOn a recent trip to the US, I learnt a surprising amount from watching the license plates on passing cars.The world around us · 14 Aug 2024
- Write to the middle of a file with Python
- Get an image from a video with ffmpeg
- Create solid-colour image placeholders to show before an image loads
- Get the embedded artwork from an MP3 file
- Convert an animated GIF to an MP4 with ffmpeg
- Build a URL with query string parameters with curl
- Using the Wikimedia Commons API to tell if a file has been deleted
- Doodling with the Mac’s command iconRotating a looped hook around a central point creates a surprising variety of shapes.Generative art · 4 Jul 2024
- How to get the selected item in Finder using AppleScript
- emptydir: look for (nearly) empty directories and delete themI’ve been writing tools in Rust again.Computers and code · 24 Jun 2024
- Google is showing outdated results from the UK’s election
- Removing letterboxing from a video screenshot with ImageMagick
- How to check the quality of a network connection
- The surprising utility of a Flickr URL parser
- How to highlight Python console sessions in Jekyll
- Drawing repetitive radial artworksUsing polar coordinates to draw leaf- and flower-like patterns that repeat in circles around a point.Generative art · 1 Jun 2024
- Not all coal is the same
- Documenting my DNS records
- Preserving pixels in Paris
- How to simulate an [Errno 54] Connection reset by peer when using pytest
- What does “insomnolent” mean?
- Open a Safari webarchive from Twitter/X without being redirected
- Creating a Safari webarchive from the command line
- Writing a file in Swift, but only if it doesn’t already exist
- What comes after AWS?
- What’s inside a Safari webarchive?
- Get a Palette colour as a command-line argument with Clap
- There’s a musical that tells you the number of minutes in a year
- How to get the filename/size without downloading a file in curl
- How to get a tally of tally counts in SQLite
- What is psephology?
- Taking regular screenshots of my website
- How to get a list of captures from the Wayback Machine
- Getting faster Jekyll builds with caching in plugins
- How to take a screenshot of a page in the Wayback Machine
- The Star-Spangled Ballad
- How to count how many Discord messages were sent on a given day
- How to see the HTTP requests being made by pywikibot
- flapi.sh: a tiny command-line tool for exploring the Flickr API
- How to embed an inline SVG in a CSS rule
- How to shuffle an array in a Jekyll template
- The new Flickr Commons Explorer
- Checking if a URL has changed when you fetch it over HTTP
- The open command can ask questions
- Why is Pillow rotating my image when I save it?
- Getting a boto3 Session for an IAM role using Python
- How to change the name of an internal link in an Obsidian table
- My config for running youtube-dl
- How much will Mastodon instances try to re-send messages?
- Step… Step… Step…
- Get my Netlify bandwidth usage from the API
- How to restrict a page to specific IP addresses
- Use the -n/-i flags to avoid overwriting files with cp and mv
- How to parse URLs in JXA
- How to check when an HTTPS certificate expires
- Using errexit and arithmetic expressions in bash
- What happens when you replace a photo on Flickr?
- The COUNT(X) function only counts non-null values
- Moving my YouTube Likes from one account to another
- Exclude files from Time Machine with tmutil addexclusion
- You need to call resp.close() to close the file opened by send_file()
- Making a PDF that’s larger than GermanyWe’re gonna need a bigger printer.Code crimes · 31 Jan 2024
- Add the -v flag to see what rm is deleting
- Custom error pages in Flask
- What characters are allowed in titles on Wikimedia Commons?
- Use the {% raw %} tag to describe Liquid in Liquid
- Beware of using test -n with command expansion
- Run a script on macOS on a schedule using a LaunchAgent
- The Collected Works of Ian Flemingo
- How to do offline geo-lookups of IP addresses
- How to move files when you need sudo on the remote server
- How to tally combinations of values across multiple columns
- How to gracefully restart a gunicorn app
- How to find the longest common suffix in a list of strings in Python
- How to simulate shell pipes with the subprocess module
- Manage MP3 metadata from iTunes with eyed3
- WordPress URLs that get hammered by spammers
- How to create a footer that’s always at the bottom of the page
- Use the {% capture %} tag to assign complex strings to variables
- Sort a list of DOM elements by sorting and calling appendChild()
- How to create flag emojis for countries in Python
- Use the IMAGE function to insert an image into a spreadsheet
- Create a directory before you cp or mv a file to it
- Pausing the animation of <svg> elements can affect child <svg> elements differently in different browsers
- How to style a <details> element differently depending on whether it’s open or closed
- Running the Netlify CLI in GitHub Actions
- Use shlex.split() to parse log files quickly
- Animate an attribute of an element with <animate>
- SVGs are only rendered on GitHub if you use an <img> that points to another file
- Get and manipulate the contents of a page in Safari with "do JavaScript"
- Use the regex library to get Unicode property escapes in Python
- Getting a tally of SQLite values as a CSV
- Run a randomly selected subset of tests with pytest
- What mammal is that?
- Python’s sqlite3 context manager doesn’t close connections
- Using sqlite-utils to convert a CSV into a SQLite database
2023 55
- My favourite books from 2023Storms and schools, escapes and exorcisms, murders and magic – what I enjoyed reading this year.Books I’ve read · 30 Dec 2023
- Getting the path to the note I have open in Obsidian
- How to delete albums
- Live Text is aware of how hyphenation works (kinda)
- Setting up Fish to make virtualenv easier
- Go between M-IDs and filenames on Wikimedia Commons
- Telling mechanize how to find local issuer certificates
- Why I prefer XML to JSON in the Wikimedia Commons APIs
- Making the fish shell more forgetful
- Hyperfocus and hobbiesThe power of ADHD-associated hyperfocus and how I’ve become wary of feeding it too often.Personal thoughts · 20 Oct 2023
- How to do resumable downloads with curl
- Find files that use a particular SDC field
- Finding the biggest items in my Photos Library
- Why the term “snak” keeps appearing in the Wikidata API
- The acronym “woe” in the Flickr API stands for “Where On Earth”
- Spotting spam in our CloudFront logs
- Adding locations to my photos from my Apple Watch workouts
- Fare-Wellcome Collection
- How I set up my Obsidian vaults
- Use Unicode property escapes to detect emoji in JavaScript
- Finding the original page for a post on Mastodon
- Finding a mystery IAM access key
- A blue plaque for HesterMaking a small memorial to a forgotten secretary who helped pull off one of the most daring deceptions of WWII.Entertainment · 19 Aug 2023
- Tag your infrastructure-as-code resources with a link to their definitions
- Parsing CloudFront logs with Python
- My Python snippet for walking a file tree
- My custom <picture> plugin for Jekyll
- Spy for Spy
- Turning off ECS tasks overnight using an EventBridge Schedule
- Preserving Dates during JSON serialisation with vanilla JS
- Writing a Mac app to review my photosDipping my toes into SwiftUI to make a tool for reviewing my photos with just the keyboard.Photo management · 18 Jun 2023
- Have a single definition of “now”
- Use concurrency gates to prevent concurrent deployments
- Starting Docker just before I need it
- Snippets to manage albums in Photos.app
- How to profile Swift code
- s3tree: viewing a tree of objects in S3 in my terminal
- Redecorating my bedroom
- Getting alerts about flaky ECS tasks in Slack
- Using templates in Terraform to document a deployment
- Publishing lots and lots of messages to SNS
- Filtering AWS CLI output by tags using jq
- Cats, cross-stitch, and copyrightIn celebration of my favourite apex predator.Art and creativity · 13 Mar 2023
- Filtering out bogus requests from Netlify Analytics
- Splitting a class into balanced groups
- Creating a Python dictionary with multiple, equivalent keys
- Testing JavaScript without a (third-party) framework
- CSS formatting in the console
- Going through my old school papers
- A Python function to iterate through an S3 Bucket Inventory
- How moving to the cloud took Wellcome’s digital collections to new heights
- A bookmarklet to show which responsive image was chosen
- Changing the bulb in a Meridian Lighting CIR100B ceiling light
- Beware of transparent backgrounds when using AVIF with ImageMagick 6
- Upward assignment in RubyA deep dive into the internals of Ruby and metaprogramming techniques, in a quest for a cursed operator.Code crimes · 13 Jan 2023
2022 46
- A script to get Live Text from images
- My favourite books from 2022Romance and rust, gender and grief, horror and the House – what I enjoyed reading this year.Books I’ve read · 31 Dec 2022
- Getting an Important Internet Checkmark to follow your cursor
- How we do bulk analysis of our Prismic content
- Launching a rocket in the worst possible wayTaking the humble <marquee> tag where no HTML tag has gone before.Code crimes · 16 Dec 2022
- Getting the base directory of an sbt project
- Redacting sensitive query parameters with koa and koa-logger
- A day out at the Bure Valley RailwayMy photos from a delightful day on a steam railway with smol trains.The world around us · 28 Nov 2022
- Changing the macOS accent colour without System Preferences
- Ten years of blogging
- Saving your alt text from Twitter
- A simple gallery plugin for Obsidian
- Agile and iterative project management
- Finding books in nearby library branches
- Some experiments with circle-based art
- Finding a tricky bug in Elasticsearch 8.4.2Gradually deleting more and more data helped me get a reliable repro for an elusive bug.Computers and code · 5 Oct 2022
- My (tiny) contribution to Rust 1.64
- “Fixing” the rules of divisionIf we want to redefine how division works, Ruby is happy to oblige.Code crimes · 23 Sep 2022
- The maths cross-stitch that hangs behind meWhen I’m on video calls, my backdrop has some maths-related art that I helped to make.Art and creativity · 22 Sep 2022
- Generating art from lattice graphsRandomly selecting a subset of edge from a graph can make pretty pictures.Generative art · 13 Sep 2022
- Finding redundant data in our Next.js props
- How to use hex colours with the palette crate
- I always want StrictUndefined in Jinja
- An Egyptian ‘mixtape’ of embroidered material
- Drawing a circular arc in an SVG
- How to customise the title of Buildkite builds triggered from GitHub deployments
- Our Place in Space
- Cut the cutesy errors
- A surprise smattering of stardom
- You should take more screenshotsIf you do any sort of creative digital work, screenshots and screen recordings are a great way to remember it.Personal thoughts · 23 Jul 2022
- A Martian plaque for a made-up plot
- One small stitch for yarn, one giant leap for yarn-kind
- Creating an Alfred Workflow to open GitHub repos
- Fictional phone numbers in For All Mankind
- Experimenting with jq as a tool for filtering JSON
- Find the dominant colours in an image in your web browser
- Running a Rust binary in Glitch
- Illustrating the cipher wheels of a Lorenz machine
- Creating a “simple” three-up image layout in CSS
- Checking lots of URLs with curl
- Beware delays in SQS metric delivery
- A tale of two Twitter cards
- Closing lots of Safari tabs with JXA
- Why is Amazon Route 53 named that way?
- Creating animated GIFs from fruit and veg
- Creating coloured bookshelf graphics in Rust
2021 37
- My favourite books from 2021Aliens and aces, railroads and racism, dating and disasters – what I enjoyed reading this year.Books I’ve read · 31 Dec 2021
- Calley-ope (calliope) Syndrome is pronouncing a word wrong because you’ve only ever read it on the page
- Why is os.sep insufficient for path operations?
- The ever-improving error messages of Rust
- A tale of two path separators
- dominant_colours, a CLI tool for finding dominant colours in an imageA new tool for playing with images.Colours · 30 Nov 2021
- Programatically finding the original filename of a photo in the macOS Photos Library
- The Oboe of Optozorax, and Other Objects
- Prevent accidentally copying the prompt character in console code snippets
- READMEs for Open Science
- Beware of incomplete PDF redactions
- How do you work with non-engineers?
- SeptembRSE: Missing narratives in discussions around diversity and inclusion
- An editing toolbar for alexwlchan.net
- Getting a monthly cloud costs report in Slack
- Operations on strings don’t always commute
- When is my EventBridge cron expression going to run next?
- Picking perfect planks with Python
- How to ignore lots of folders in Spotlight
- Markdown’s gentle encouragement towards accessible images
- Finding misconfigured or dangling CloudWatch Alarms
- A few useful GitHub searches
- Listing deleted secrets in AWS Secrets Manager with boto3 and the AWS CLI
- Visualising how often I write in my journal
- Downloading objects from/uploading files to S3 with progress bars in Python
- Drawing coloured squares/text in my terminal with Python
- Using AppleScript to detect if a Safari window uses Private Browsing
- Finding the app/process that’s using Secure Input
- Getting a list of URLs from my Safari history
- Building Wellcome Collection’s new unified catalogue search
- Drawing inner/outer strokes in SVG (clips and masks)Showing how clips and masks work, and how they can be used to draw inner and outer strokes of a shape.Drawing things · 12 Mar 2021
- Creating pairs of interlocking rainbow hearts 🌈
- An AppleScript to toggle Voice Control
- Screaming in the Cloud: Using the Cloud to Preserve the Future
- How KempisBot worksHow I reincarnated a fifteenth-century monk and taught him to use Twitter.Fun stuff · 31 Jan 2021
- Wellcome Collection’s approach to digital verificationHow we ensure the safety, security and integrity of the files in Wellcome’s digital collections.Digital preservation · 18 Jan 2021
- What year is it? (A tale of ISO week dates)
2020 60
- Creating short-lived, temporary roles for experimenting with AWS IAM policy documents
- “Non-technical users”
- A script to copy images from Docker Hub to Amazon ECR
- How I read non-fiction books
- Maths is about facing ambiguity, not avoiding it
- Remembering if a <details> element was opened
- A Python function to ignore a path with .git/info/exclude
- Use git check-ignore to debug your .gitignore
- How do I use my iPhone cameras?
- A new README for docstore, my tool for organising scanned paperwork
- The danger of bad error messages
- Replicating Wellcome Collection’s digital archive to Azure Blob Storage
- Using qlmanage to create thumbnails on macOS
- Two Python functions for getting CloudTrail events
- S3 prefixes are not directories
- S3 keys are not file paths
- Using fuzzy string matching to find duplicate tags
- Getting a Markdown link to a window in Safari
- Why do programming languages have a main() function?
- Running concurrent Try functions in Scala
- What does \d match in a regex?
- How to do parallel downloads with youtube-dl
- Changing the accent colour of ICNS icons
- Using AppleScript to open a URL in Private Browsing in Safari
- Fat shaming in The Good Place
- Large things living in cold placesUsing cold storage tiers to reduce the cost of storing Wellcome’s digital collections in the cloud.Digital preservation · 17 Jun 2020
- Archive monocultures considered harmful
- Finding the months between two dates in Python
- Getting every item from a DynamoDB table with Python
- Showing human-friendly dates in JavaScript
- The Sachsenhausen concentration camp
- Moving messages between SQS queues
- Downloading the AO3 fics that I’ve saved in Pinboard
- Social media as a growth culture for opinions
- Taking tuple unpacking to terrible places
- Make it safe to admit mistakes
- Give your audience time to react
- How long is my data? 💾A fun app to calculate the size of your data in terms of floppy disks.Fun stuff · 2 May 2020
- Illustrating lifecycle transitions in Amazon S3A picture speaks a thousand words, which is why I always have pen and paper to hand.AWS · 1 May 2020
- Using DynamoDB as a calculatorTaking advantage of Amazon’s least-loved compute platform.Code crimes · 27 Apr 2020
- Exploring an unknown SQL server
- Thinking about your gender
- Comparing JSON strings when testing in Scala
- Complex systems have complex failures
- Adventures in euphoria and embodiment
- Access to information is a privilege
- A snippet for downloading files with Python
- Storing language vocabulary as a graph
- Inclusion can’t be an afterthought
- Rich enough to make bad choices
- Sick leave while working from home
- Creating striped flag wallpapers with Pillow
- A Jekyll filter for adding non-breaking spaces
- Adjusting the dominant colour of an image
- Storing multiple, human-readable versions of BagIt bags
- Building Wellcome Collection’s new archival storage serviceHow we built the new cloud-based storage system for holding an ever-growing digital archive.Wellcome Collection · 5 Feb 2020
- An interactive map of British railway stations 🚂
- A deletion canary: testing your S3 bucket permissions
- Generating pride-themed Norse valknuts with Python 🌈
- Finding the CPU and memory bottlenecks in an ECS cluster
2019 49
- This YAML file will self-destruct in five seconds!
- Some useful spreadsheet functions: FORMULATEXT, MATCH, CONCATENATE and INDIRECT
- November 2019 scripts: downloading podcasts, retrying flaky errors, Azure and AWS
- How I scan and organise my paperwork
- Saving a copy of a tweet by typing ;twurl
- An AWS costs graph that works for me
- Digital preservation at Wellcome Collection
- Adventures in Python with concurrent.futuresHow I use concurrent.futures to speed up my batch scripting in Python.Python · 16 Oct 2019
- Sans I/O programming: what, why and how (PyCon UK talk)
- Adding religious holidays to my calendar
- The rough edges of filecmp
- Experiment: GitHub code search with de-duplication
- Drawing with triangular coordinates in SVG
- Streaming large objects from S3 with ranged GET requests
- Iterating over the entries of a compressed archive (tar.gz) in Scala
- Installing mimetype on Alpine Linux
- Getting a tint colour from an image with Python and k‑meansI want to display text next to an image that looks visually similar to the image, so I need to extract a tint colour.Colours · 19 Aug 2019
- Finding divisors of a number with Python
- Creating preview thumbnails of PDF documents
- Listing even more keys in an S3 bucket with Python
- Ten braille facts / ⠼⠁⠚⠀⠃⠗⠁⠊⠇⠇⠑⠀⠋⠁⠉⠞⠎
- Deleting nested JSON fields with Circe optics
- An inescapable conclusion
- A Jekyll filter for obfuscating email addresses
- Reading a Chinese dictionary / 读一本中文字典
- Converting Acorn images on the command-line
- A script for getting cover images from mobi ebooks
- Hey Apple, cycle tracking isn’t just for women
- Questions to ask when writing a trans inclusion policy
- Falsehoods programmers believe about Unix time
- Creating a locking service in a Scala type class
- How do Dreamwidth posts IDs work?
- Add a Git co-author credit with “Co-authored-by” in your commit message
- Conditional updates on nested fields in DynamoDB
- How to iterate over the lines of an InputStream in Scala
- Using TransportAPI and geopy to get the distance between stations
- Reversing a t.co URL to the original tweet
- Some tips for conferences
- Manipulating URL query parameters in JavaScript
- The Content-Disposition header can be used to name a downloaded file
- Getting a transcript of a talk from YouTube
- Creating a GitHub Action to auto-merge pull requests
- A day out to the Forth BridgePhotographs from a trip to North Queensferry to see the Forth Bridge, the Forth Road Bridge, and an unexpected light tower.The world around us · 15 Mar 2019
- Finding the latest screenshot in macOS Mojave
- Atomic, cross-filesystem moves in Python
- Checking Jekyll sites with HTMLProofer
- Working with really large objects in S3
- The Curb Cut Effect
- Notes from You Got This 2019
2018 39
- Notes on reading a UTF‑8 encoded CSV in Python
- Iterating in fixed-size chunks in Python
- Getting credentials for an assumed IAM Role
- A script for backing up Tumblr posts and likes
- How to suppress installing rdoc/ri docs when running gem install
- Keeping track of my book recommendations
- Replace black/white parts of an image with ImageMagick
- My visit to the Aberdulais Falls
- Finding SNS topics without any subscriptions
- Peering through MRI scans of fruit and veg
- How to set the clock on a Horstmann Electronic 7 water heater
- Create compact JSON with Python
- How to use xargs for parallel processing
- Pushing with --force-with-lease is safer than with --force
- Custom 404 responses in Finatra
- Assume worst intent (designing against the abusive ex)
- Building trust in an age of suspicious minds
- Signs of the time
- List all Git object IDs and their type
- A basic error logger for Python Lambdas
- Making the venue maps for PyCon UK
- Implementing parallel scan in DynamoDB with Scanamo
- Avoiding the automatic redirect on Tumblr posts
- Ideas for inclusive conferences and events
- A robot leaked my SSH keys
- My favourite iMac accessory
- Drawing ASCII bar charts
- Creating a data store from S3 and DynamoDB
- Beware of logged errors from subprocess
- Two shortcuts for using S3 in the shell
- (Anti) Social Media
- Python throws a TypeError if you return a non-string from a custom __repr__ or __str__ method
- Notes on A Plumber’s Guide to Git
- The Hypothesis continuous release process
- Keep an overnight bag in the office
- Getting helpful CloudWatch alarms in Slack
- Getting every message in an SQS queue
- Listing keys in an S3 bucket with Python, redux
- IP and DNS addresses for documentation
2017 22
- Your repo should be easy to build, and how
- Using Loris for IIIF at Wellcome
- Pruning old Git branches
- Downloading logs from Amazon CloudWatch
- Don’t tap the mic, and other tips for speakers
- My favourite WITCH story
- A plumber’s guide to Git
- Using privilege to improve inclusion
- Lightning talks
- Using hooks for custom behaviour in requests
- Four ways to underline text inLaTeK LaTeXI’m very picky about the way underlines look, and have spent a lot of time trying to get the perfect underline inLaTeK LaTeX.LaTeKLaTeX · 11 Oct 2017
- Using pip-tools to manage my Python dependencies
- Some useful Git commands for CI
- Ode to docopt
- A Python module for lazy reading of file objects
- Backing up full-page archives from Pinboard
- Listing keys in an S3 bucket with Python
- Backing up content from SoundCloud
- A visit to the Crossness pumping stationPictures from my recent visit to Crossness, a Victorian pumping station for London’s sewers.The world around us · 7 Jun 2017
- Accessibility at AlterConf
- A Python interface to AO3
- Experiments with AO3 and Python
2016 29
- Another example of why strings are terrible
- Why does Hypothesis try the same example three times before failing?
- Some low-tech ways to get more ideas
- Use keyring to store your credentials
- Rust macros are smarter than just text substitution
- Creating low contrast wallpapers with Pillow
- Tiling the plane with Pillow
- How to set the clock on a Tricity Bendix SIE454 electric cooker
- Why I use py.test
- How to set the clock on a Worcester 28CDi boiler
- A shell alias for tallying data
- My travelling tech bag
- aspell, a command-line spell checker
- Silence is golden
- Live captioning at conferences
- You can use shutil.which to check if an executable is in your PATH
- Python snippets: Cleaning up empty/nearly empty directories
- Python snippets: Chasing redirects and URL shorteners
- Clearing disk space on OS X
- Introduction to property-based testing
- Finding 404s and broken pages in my Apache logs
- A Python smtplib wrapper for Fastmail
- Safely deleting a file called ‘-rf *’
- Treat regular expressions as code, not magic
- Get images from the iTunes/App/Mac App Stores with Alfred
- Exclusively create a file in Python 3
- Germany has longer telephone numbers than the rest of the world
- How I use TextExpander to curb my language
- The Skeletor clip loop, 2015 edition
2015 15
- Pretty printing JSON and XML in the shell
- Backups and Docker
- Export a list of URLs from Safari Reading List
- Python and the BBC micro:bit
- Quick shell access for Docker containers
- Review: Effective Python
- Finding even more untagged posts on Tumblr
- Useful Bash features: exit traps
- Safer file copying in Python
- Useful Git features: a per-clone exclude file (.git/info/exclude)
- Cloning GitHub’s Contributions chart
- Some exam advice
- Pygmentizr
- Tidying up my 1Password
- Adding checkboxes to lists
2014 12
- Kitchen sink security
- Acronyms
- Skeletors All the Way Down
- Unpacking sets and ranges from a single string
- Notes on Tumblr
- Playing with 404 pages
- My new standing desk
- Updates to my site for finding untagged Tumblr posts
- A quick Alfred workflow for opening recent screenshots
- Getting plaintextLaTeK LaTeX from Wolfram Alpha
- Skeletor!
- Finding untagged posts on Tumblr, redux