<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Matt White | SWE</title>
    <link>https://matt-w.net/</link>
    <description>Recent content on Matt White | SWE</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Wed, 27 Dec 2023 00:00:00 +0000</lastBuildDate>
    
        <atom:link href="https://matt-w.net/index.xml" rel="self" type="application/rss+xml" />
    
    
    
    <item>
      <title>Azure&#39;s Browser Bias</title>
      <link>https://matt-w.net/blog/azure-browser-bias/</link>
      <pubDate>Wed, 27 Dec 2023 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/azure-browser-bias/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been tinkering with the Azure cloud recently.
On any cloud provider, I try to configure my hardware keys as a method of multi-factor authentication.&lt;/p&gt;
&lt;p&gt;Microsoft&amp;rsquo;s authentication apparatus is much more abrasive than either Google&amp;rsquo;s or AWS&amp;rsquo;s.
Anyone with a nonstandard setup is used to companies aggressively locking their accounts - I&amp;rsquo;m never surprised when I have to jump through additional hoops.&lt;/p&gt;
&lt;p&gt;It is always more grating when a company&amp;rsquo;s security theater comes with &lt;a href=&#34;https://support.microsoft.com/en-us/account-billing/account-has-been-locked-805e8b0d-4141-29b2-7b65-df6ff6c9ce27&#34;&gt;unfounded TOS violation accusations&lt;/a&gt;:&lt;/p&gt;

 &lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2023/microsoft-browser-bias/azure-accusations.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2023/microsoft-browser-bias/azure-accusations.webp&#34; class=&#34;post-image &#34; alt=&#34;Microsoft hurling accusations of malware, phishing, spaming, and hacking at any user who hasn&amp;#39;t logged in from their browser recently.&#34; width=&#34;600px&#34; style=&#34;max-width:600px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I plead guilty to the crime of logging in from an unfamiliar browser.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;But that&amp;rsquo;s all uninteresting.
Twitter does it.
Meta does it.
Microsoft does it.
What is Microsoft doing that&amp;rsquo;s unusual?&lt;/p&gt;
&lt;h1 id=&#34;user-agent-determines-login-success&#34;&gt;User Agent Determines Login Success&lt;/h1&gt;
&lt;p&gt;Microsoft will reject valid credentials depending on which combination of browser/operating system they think you&amp;rsquo;re using.&lt;/p&gt;
&lt;p&gt;I worked through Microsoft&amp;rsquo;s hardware key enrollment process.
They won&amp;rsquo;t let you use hardware keys as multi-factor - they only let you use it as a password alternative.
You can select a security key as your login method.&lt;/p&gt;

 &lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2023/microsoft-browser-bias/azure-login-select.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2023/microsoft-browser-bias/azure-login-select.webp&#34; class=&#34;post-image &#34; alt=&#34;Azure security key login selection menu&#34; width=&#34;600px&#34; style=&#34;max-width:600px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;All my login attempts from Firefox consistently failed.&lt;/p&gt;

 &lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2023/microsoft-browser-bias/azure-key-failure.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2023/microsoft-browser-bias/azure-key-failure.webp&#34; class=&#34;post-image &#34; alt=&#34;Azure giving me an arbitrary failure because they dislike my user agent&#34; width=&#34;600px&#34; style=&#34;max-width:600px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;But if I switched my user agent to say I was using Chrome, access granted.&lt;/p&gt;

 &lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2023/microsoft-browser-bias/azure-login-success.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2023/microsoft-browser-bias/azure-login-success.webp&#34; class=&#34;post-image &#34; alt=&#34;Azure login succeeding after switching user agents to Chrome&#34; width=&#34;600px&#34; style=&#34;max-width:600px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;h1 id=&#34;what-browsers-do-our-benevolent-lord-microsoft-deem-acceptable&#34;&gt;What Browsers Do Our Benevolent Lord Microsoft Deem Acceptable?&lt;/h1&gt;
&lt;p&gt;This got me curious, so I went down the list of user agent presets I had - seeing which would succeed and which Microsoft would arbitrarily reject.&lt;/p&gt;
&lt;p&gt;I had expected to see a pattern like &amp;ldquo;always reject linux&amp;rdquo; or &amp;ldquo;only accept chromium&amp;rdquo;, but the actual behavior was a bit more varied.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Observations&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Every attempt was conducted on a Linux 6.5.7-arch1-1 x86_64 system from Firefox 118.0.2 (64-bit).&lt;/em&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Logging in from OSX or iOS always succeeded - not a shock, given how many companies issue Apple products as standard kit.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Logging in from Windows always succeeded unless the browser was internet explorer.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Firefox&amp;rsquo;s success depended entirely on the operating system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Android logins always failed - but Android logins from Chrome produced an error that I didn&amp;rsquo;t see from any other user agent:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2023/microsoft-browser-bias/azure-android-failure.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2023/microsoft-browser-bias/azure-android-failure.webp&#34; class=&#34;post-image &#34; alt=&#34;Azure special error for chrome on android&#34; width=&#34;600px&#34; style=&#34;max-width:600px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;h1 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;There are a lot of user agent combinations that I haven&amp;rsquo;t tested (especially around Microsoft Edge), but given the miniscule odds that Microsoft will ever try to mitigate this behavior, being more thorough would be a waste.&lt;/p&gt;
&lt;p&gt;I doubt this behavior is intentional, but since I won&amp;rsquo;t be changing my browser or operating system selection to suit Microsoft, I&amp;rsquo;ll steer clear of Azure when I have the choice.&lt;/p&gt;
&lt;h1 id=&#34;tested-user-agents&#34;&gt;Tested User Agents&lt;/h1&gt;
&lt;h2 id=&#34;failed-logins&#34;&gt;Failed logins:&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Linux / Firefox 120&lt;/strong&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/120.0
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Android Phone / Firefox 120&lt;/strong&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Mozilla/5.0 (Android 14; Mobile; rv:109.0) Gecko/120.0 Firefox/120.0
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Android Tablet / Firefox 120&lt;/strong&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Mozilla/5.0 (Android 14; Tablet; rv:109.0) Gecko/120.0 Firefox/120.0
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows / IE 11&lt;/strong&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Android Phone / Chrome 120&lt;/strong&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Mozilla/5.0 (Linux; Android 14; Z832 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.119 Mobile Safari/537.36
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Android Tablet / Chrome 120&lt;/strong&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Mozilla/5.0 (Linux; Android 14; SAMSUNG-SM-T377A Build/NMF26X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.119 Mobile Safari/537.36
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h2 id=&#34;successful-logins&#34;&gt;Successful logins:&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Windows / Firefox 120&lt;/strong&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/120.0
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mac OS X / Safari 17&lt;/strong&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15`
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mac OS X / Firefox 120&lt;/strong&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 13_6) Gecko/20100101 Firefox/120.0
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Linux / Chrome 120&lt;/strong&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Mozilla/5.0 (X11; Linux x86_64; rv:109.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.119 Safari/537.36
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows / Chrome 120&lt;/strong&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.119 Safari/537.36
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows / Firefox 115 ESR&lt;/strong&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;iPhone / Safari 17&lt;/strong&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Mozilla/5.0 (iPhone; CPU OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/14E304 Safari/605.1.15
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;iPad / Safari 17&lt;/strong&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Mozilla/5.0 (iPad; CPU OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/605.1.15
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>LUKS-Encrypted Arch with systemd-boot and SecureBoot</title>
      <link>https://matt-w.net/blog/luks-arch-secureboot/</link>
      <pubDate>Tue, 05 Dec 2023 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/luks-arch-secureboot/</guid>
      <description>&lt;p&gt;When I was getting started with Arch linux, I quickly learned that there were a lot of tricky parts that no amount of studying the &lt;a href=&#34;https://wiki.archlinux.org/title/Installation_guide&#34;&gt;installation guide&lt;/a&gt; could get me through.
I ran into a lot of problems that I could only sort through by reading other people&amp;rsquo;s system writups.&lt;/p&gt;
&lt;p&gt;I just ran through a complete system rebuild today and figured it was time to do my own writup.&lt;/p&gt;
&lt;h1 id=&#34;live-usb&#34;&gt;Live USB&lt;/h1&gt;
&lt;p&gt;Create and boot from an Arch &lt;a href=&#34;https://archlinux.org/download/&#34;&gt;live USB&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;ssh-optional&#34;&gt;SSH (Optional)&lt;/h2&gt;
&lt;p&gt;If you have another system you can SSH in from, it&amp;rsquo;ll allow you to copy/paste commands.&lt;/p&gt;
&lt;p&gt;Create a password for the root user on the live USB

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;passwd
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Enable the ssh service

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;systemctl start sshd.service
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;You can find the ip address of the system:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;ip addr show
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;The output can be a bit verbose. You can try to narrow it down by piping to grep.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;ip addr show | grep &amp;#34;state UP&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Use that ip address to ssh in from a different system:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;ssh root@&amp;lt;ip-address-you-found&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h2 id=&#34;check-the-boot-mode&#34;&gt;Check the Boot Mode&lt;/h2&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;cat /sys/firmware/efi/fw_platform_size
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For my system, the output is &lt;code&gt;64&lt;/code&gt;, which indicates that the system is in UEFI mode in 64-bit.
A &lt;code&gt;32&lt;/code&gt; would indicate UEFI mode in 32-bit.&lt;/p&gt;
&lt;p&gt;If the output is anything else, you&amp;rsquo;ve got to go messing with your motherboard.&lt;/p&gt;
&lt;h2 id=&#34;create-the-partitions&#34;&gt;Create the Partitions&lt;/h2&gt;
&lt;h3 id=&#34;list-disks-and-partitions&#34;&gt;List Disks and Partitions&lt;/h3&gt;
&lt;p&gt;To identify the disk(s) you want to use during the installation:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;lsblk
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Example output:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;NAME          MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
sdb             8:0    0 465.8G  0 disk  
sda             8:16   0   9.1T  0 disk 
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;For our purposes, let&amp;rsquo;s use &lt;code&gt;sdb&lt;/code&gt; as the target disk.&lt;/p&gt;
&lt;h3 id=&#34;create-the-partitions-1&#34;&gt;Create the Partitions&lt;/h3&gt;
&lt;p&gt;Using &lt;code&gt;gdisk&lt;/code&gt; for partitioning.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;gdisk /dev/sdb
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Create 3 partitions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Partition 1 - Code EA00 - XBOOTLDR partition - 512MB&lt;/li&gt;
&lt;li&gt;Partition 2 - Code EF00 - EFI System Partition - 256MB&lt;/li&gt;
&lt;li&gt;Partition 3 - Code 8309 - Linux LUKS - All the remaining space&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since &lt;code&gt;gdisk&lt;/code&gt; is interactive, I have to show this awkward collection of key presses.
Each use of &lt;code&gt;[Enter]&lt;/code&gt; below indicates that I&amp;rsquo;m taking the default value.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;# Create a new GUID Partition Table (GPT)
o
Y

# Create the XBOOTLDR partition
n
[Enter]
0
&amp;#43;512M
ea00

# Create the EFI partition
n
[Enter]
[Enter]
&amp;#43;256M
ef00

# Create the LUKS partition
n
[Enter]
[Enter]
[Enter]
8309

# Persist the changes
w
Y
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h3 id=&#34;setup-luks&#34;&gt;Setup LUKS&lt;/h3&gt;
&lt;p&gt;Use luks to encrypt and open the 3rd partition:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;cryptsetup luksFormat \
    --use-random \
    -S 1 \
    -s 512 \
    -h sha512 \
    -i 5000 \
    /dev/sdb3
cryptsetup open /dev/sdb3 cryptlvm
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h3 id=&#34;setup-volumes&#34;&gt;Setup Volumes&lt;/h3&gt;
&lt;p&gt;First, create the physical volume.
I use the name &lt;code&gt;cryptlvm&lt;/code&gt;, but the name is arbitrary.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;pvcreate /dev/mapper/cryptlvm
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Create a volume group:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;vgcreate vg /dev/mapper/cryptlvm
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Now create logical volumes for swap, root, and home.&lt;/p&gt;
&lt;p&gt;Use the appropriate amount of space for root, which is where installed packages will live by default.
128G is enough for me right now, but for you it may be too small.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;lvcreate -L 8G vg -n swap
lvcreate -L 128G vg -n root
lvcreate -l 100%FREE vg -n home
&lt;/code&gt;&lt;/pre&gt;
&lt;em&gt;NOTE the case-sensitivity of arguments. The &amp;lsquo;-l&amp;rsquo; is not a typo for &amp;lsquo;-L&amp;rsquo;.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;format-the-partitions&#34;&gt;Format the Partitions&lt;/h3&gt;
&lt;p&gt;Using FAT32 for the efi and boot partitions - they need to be readable by the firmware.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;mkfs.fat -F32 /dev/sdb1
mkfs.fat -F32 /dev/sdb2
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Use the old reliable ext4 for root and home.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;mkfs.ext4 /dev/vg/root
mkfs.ext4 /dev/vg/home
mkswap /dev/vg/swap
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h3 id=&#34;mounting&#34;&gt;Mounting&lt;/h3&gt;
&lt;p&gt;Mount the root partition.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;mount /dev/vg/root /mnt
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Create the nested mount points.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;mkdir /mnt/home
mkdir /mnt/boot
mkdir /mnt/efi
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;And mounting

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;mount /dev/vg/home /mnt/home
mount /dev/sdb1 /mnt/boot
mount /dev/sdb2 /mnt/efi
swapon /dev/vg/swap
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;At this point, &lt;code&gt;lsblk&lt;/code&gt; should produce output that looks similar to this:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;NAME          MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
sdb             8:0    0 465.8G  0 disk  
├─sdb1          8:1    0   512M  0 part  /boot
├─sdb2          8:2    0   256M  0 part  /efi
└─sdb3          8:3    0 465.2G  0 part  
  └─cryptlvm  254:0    0 465.2G  0 crypt 
    ├─vg-swap 254:1    0     8G  0 lvm   [SWAP]
    ├─vg-root 254:2    0   128G  0 lvm   /
    └─vg-home 254:3    0 329.2G  0 lvm   /home
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h2 id=&#34;setup-initial-installation&#34;&gt;Setup Initial Installation&lt;/h2&gt;
&lt;h3 id=&#34;packages&#34;&gt;Packages&lt;/h3&gt;
&lt;p&gt;Alter the mirror list, if you have any preferences.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;vim /etc/pacman.d/mirrorlist
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Install any essential programs.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;pacman -Sy archlinux-keyring
pacstrap /mnt \
    base \
    linux \
    linux-firmware \
    base-devel \
    git \
    sudo \
    archlinux-keyring \
    lvm2 \
    mkinitcpio \
    vim \
    dhcpcd \
    openssh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;configure-fstab&#34;&gt;Configure fstab&lt;/h3&gt;
&lt;p&gt;This will copy your current setup for all the mounted volumes/partitions/etc and save it to fstab so that it can be setup automatically on boot.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;genfstab -U /mnt &amp;gt;&amp;gt; /mnt/etc/fstab
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&#34;chroot-into-the-installation&#34;&gt;Chroot Into the Installation&lt;/h1&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;arch-chroot /mnt
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;update-root-password&#34;&gt;Update Root Password&lt;/h2&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;passwd
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;create-user-and-basic-configuration&#34;&gt;Create User and Basic Configuration&lt;/h2&gt;
&lt;p&gt;Create a user and set the password.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;useradd -m matt
passwd matt
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Add the user to the &lt;code&gt;wheel&lt;/code&gt; group, which will determine who has access to &lt;code&gt;sudo&lt;/code&gt;.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;gpasswd -a matt wheel
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;visudo&lt;/code&gt;, which will open a file where you can add wheel users to sudoers. Uncomment this relevant section:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;## Uncomment to allow members of group wheel to execute any command
%wheel ALL=(ALL) ALL
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h2 id=&#34;setting-the-clock&#34;&gt;Setting the Clock&lt;/h2&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;timedatectl set-ntp true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adjust to your own time zone as-needed.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;ln -sf /usr/share/zoneinfo/America/Denver /etc/localtime
hwclock --systohc
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h2 id=&#34;locale&#34;&gt;Locale&lt;/h2&gt;
&lt;p&gt;Uncomment &lt;code&gt;en_US.UTF-8 UTF-8&lt;/code&gt; in &lt;code&gt;/etc/locale.gen&lt;/code&gt; (or whatever line applies to you) and generate locale:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;locale-gen
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;And similar for locale configuration:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;echo &amp;#34;LANG=en_US.UTF-8&amp;#34; &amp;gt;&amp;gt; /etc/locale.conf
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h2 id=&#34;networking&#34;&gt;Networking&lt;/h2&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;echo &amp;#34;myhostname&amp;#34; &amp;gt;&amp;gt; /etc/hostname
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update &lt;code&gt;/etc/hosts&lt;/code&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;127.0.0.1	localhost
::1		ip6-localhost
127.0.1.1	myhostname.localdomain	myhostname
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Enable dhcpcd and ssh so everything works on the system&amp;rsquo;s first reboot

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;systemctl enable dhcpcd
systemctl enable sshd
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h3 id=&#34;local-hostname-resolution&#34;&gt;.local Hostname Resolution&lt;/h3&gt;
&lt;p&gt;I have a ton of machines on my local network that I need to access on a regular basis.
Local hostname resolution helps avoid hardcoding IP addresses everywhere.&lt;/p&gt;
&lt;p&gt;Setup local hostname resolution with &lt;a href=&#34;https://wiki.archlinux.org/title/Avahi#Hostname_resolution&#34;&gt;Avahi&lt;/a&gt;.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;pacman -Sy avahi nss-mdns
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Update hosts in &lt;code&gt;/etc/nsswitch.conf&lt;/code&gt; to add mdns.
The main thing is to make sure &lt;code&gt;mdns_minimal [NOTFOUND=return]&lt;/code&gt; gets added somewhere between &lt;code&gt;files&lt;/code&gt; and &lt;code&gt;dns&lt;/code&gt;.
Resolution will check sources in order.
We don&amp;rsquo;t want it querying dns for local requests.&lt;/p&gt;
&lt;p&gt;
&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;hosts: mymachines resolve [!UNAVAIL=return] files myhostname mdns_minimal [NOTFOUND=return] dns
&lt;/code&gt;&lt;/pre&gt;
&lt;em&gt;note that &lt;code&gt;myhostname&lt;/code&gt; and &lt;code&gt;mymachines&lt;/code&gt; are actual values, not placeholders&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Enable avahi

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;systemctl enable avahi-daemon
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h2 id=&#34;initramfs&#34;&gt;Initramfs&lt;/h2&gt;
&lt;h3 id=&#34;create-a-keyfile&#34;&gt;Create a Keyfile&lt;/h3&gt;
&lt;p&gt;A keyfile lets us bypass unnecessary passwords on boot.
Create one and add it as a LUKS key.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;mkdir /root/secrets
chmod 700 /root/secrets
head -c 64 /dev/urandom &amp;gt; /root/secrets/crypto_keyfile.bin
chmod 600 /root/secrets/crypto_keyfile.bin
cryptsetup -v luksAddKey -i 1 /dev/sdb3 /root/secrets/crypto_keyfile.bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Find the partition&amp;rsquo;s UUID value (&lt;strong&gt;and keep it on hand&lt;/strong&gt;, we need it a few times).
It will be one of these:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;blkid | grep crypto_LUKS
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Add this info to &lt;code&gt;/etc/crypttab&lt;/code&gt;:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;cryptlvm       UUID=&amp;lt;YOUR UUID&amp;gt; /root/secrets/crypto_keyfile.bin luks
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h3 id=&#34;update-mkinitcpio&#34;&gt;Update mkinitcpio&lt;/h3&gt;
&lt;p&gt;We need to modify &lt;code&gt;/etc/mkinitcpio.conf&lt;/code&gt; and add some hooks.
lvm2 is required since this enables the initramfs to properly manage LVM volumes during the boot process.
encrypt is required since we&amp;rsquo;re using LUKS.
filesystems is required for ext4, but should already be there.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a section for HOOKS, which should end up looking similar to this:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;HOOKS=(base udev autodetect keyboard modconf block encrypt lvm2 filesystems fsck)
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h3 id=&#34;create-the-initramfs-image&#34;&gt;Create the initramfs Image&lt;/h3&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;mkinitcpio -p linux
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;add-microcode-supporthttpswikiarchlinuxorgtitlemicrocodeinstallation&#34;&gt;Add &lt;a href=&#34;https://wiki.archlinux.org/title/Microcode#Installation&#34;&gt;Microcode Support&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;You can check your processor with

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;lscpu
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Based on the results, install either &lt;code&gt;amd-ucode&lt;/code&gt; or &lt;code&gt;intel-ucode&lt;/code&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;pacman -S &amp;lt;your-ucode-type&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note this value, which is used in the bootctl configuration, below.&lt;/p&gt;
&lt;h2 id=&#34;configure-the-bootloader&#34;&gt;Configure the Bootloader&lt;/h2&gt;
&lt;p&gt;So far I prefer &lt;a href=&#34;https://wiki.archlinux.org/title/Systemd-boot&#34;&gt;systemd-boot&lt;/a&gt; to GRUB, which I was never able to get to cooperate with SecureBoot or LUKS2.&lt;/p&gt;
&lt;p&gt;Create an entry at &lt;code&gt;/boot/loader/entries/arch.conf&lt;/code&gt;:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;title Arch Linux
linux /vmlinuz-linux
initrd /&amp;lt;YOUR-UCODE-TYPE&amp;gt;.img
initrd /initramfs-linux.img
options cryptdevice=UUID=&amp;lt;UUID-OF-ROOT-PARTITION&amp;gt;:cryptlvm root=/dev/vg/root rw cryptkey=rootfs:/root/secrets/crypto_keyfile.bin
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Configure &lt;code&gt;/efi/loader/loader.conf&lt;/code&gt;:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;default  arch.conf
timeout  5
console-mode max
editor   no
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;You may want a lower timeout value; it dictates how many seconds the loader selection screen shows on boot.&lt;/p&gt;
&lt;h3 id=&#34;sanity-check&#34;&gt;Sanity Check&lt;/h3&gt;
&lt;p&gt;Run &lt;code&gt;bootctl&lt;/code&gt; with no params.
It should tell you if your configuration is obviously broken.&lt;/p&gt;
&lt;h2 id=&#34;restart-the-system&#34;&gt;Restart the System&lt;/h2&gt;
&lt;p&gt;Exit chroot and reboot.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;exit
umount -a /mnt
reboot
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;The new installation should be able to boot via systemd-boot without the live usb.&lt;/p&gt;
&lt;h1 id=&#34;secure-boot&#34;&gt;Secure Boot&lt;/h1&gt;
&lt;p&gt;Boot up normally without the live USB.&lt;/p&gt;
&lt;p&gt;Run as root:
&lt;code&gt;sudo su&lt;/code&gt;&lt;/p&gt;
&lt;h2 id=&#34;check-the-current-status&#34;&gt;Check the Current Status&lt;/h2&gt;
&lt;p&gt;Install &lt;a href=&#34;https://github.com/Foxboron/sbctl&#34;&gt;sbctl&lt;/a&gt;:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;pacman -S sbctl
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Check if everything is ready by running:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sbctl status
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;These are the states we&amp;rsquo;re looking for.
Setup mode, no secure boot, sbctl installed.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Installed:	    ✓ sbctl is installed
Setup Mode:	    ✗ Enabled
Secure Boot:	✗ Disabled
&lt;/code&gt;&lt;/pre&gt;
&lt;em&gt;This can still go fine if sbctl doesn&amp;rsquo;t realize that its installed.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;disable-secure-boot-and-enter-setup-mode&#34;&gt;Disable Secure Boot and Enter Setup Mode&lt;/h2&gt;
&lt;p&gt;If the status isn&amp;rsquo;t what we want, we need to update the firmware settings.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;systemctl reboot --firmware
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When your system boots into the firmware, disable secure boot and enable setup mode/remove existing keys.&lt;/p&gt;
&lt;p&gt;Unfortunately some motherboards make this quite hard to find.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll want to set your UEFI firmware supervisor (administrator) password in the firmware settings;
that way an attacker can&amp;rsquo;t simply boot into UEFI setup utility and turn off SecureBoot.&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t use the same UEFI firmware supervisor password as your encryption password for LUKS.
On some old systems, the supervisor password can be recovered as plaintext from the EEPROM chip.&lt;/p&gt;
&lt;p&gt;Restart.&lt;/p&gt;
&lt;h2 id=&#34;enroll-the-keys&#34;&gt;Enroll the Keys&lt;/h2&gt;
&lt;p&gt;When you&amp;rsquo;re booted back into the new system:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sbctl create-keys
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Enroll the keys

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sbctl enroll-keys -m
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Assuming that went well, we need to sign certain EFI files.
To identify the files we need to sign, we use &lt;code&gt;sbctl verify&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The lazy way I use:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sbctl verify | grep &amp;#39;✗&amp;#39; | cut -d &amp;#39; &amp;#39; -f 2 | xargs -n 1 sbctl sign -s
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Need to sign these as well.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sbctl sign -s /boot/vmlinuz-linux

sbctl sign -s \
    -o /usr/lib/systemd/boot/efi/systemd-bootx64.efi.signed \
    /usr/lib/systemd/boot/efi/systemd-bootx64.efi
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;sbctl verify&lt;/code&gt; again to ensure everything is kosher.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;$ sbctl verify
Verifying file database and EFI images in /efi...
✓ /boot/vmlinuz-linux is signed
✓ /efi/EFI/BOOT/BOOTX64.EFI is signed
✓ /efi/EFI/systemd/systemd-bootx64.efi is signed
✓ /usr/lib/systemd/boot/efi/systemd-bootx64.efi.signed is signed
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h2 id=&#34;turn-on-secureboot&#34;&gt;Turn on SecureBoot&lt;/h2&gt;
&lt;p&gt;Back to firmware settings.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;systemctl reboot --firmware
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Turn on Secure Boot in the settings and save.
If all is well, your system should boot successfully.&lt;/p&gt;
&lt;h2 id=&#34;check-if-secure-boot-worked&#34;&gt;Check if Secure Boot Worked&lt;/h2&gt;
&lt;p&gt;Now to check if everything worked out.
&lt;code&gt;sbctl status&lt;/code&gt; can help:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;$ sbctl status
Installed:	✓ sbctl is installed
Owner GUID:	&amp;lt;some-guid&amp;gt;
Setup Mode:	✓ Disabled
Secure Boot:	✓ Enabled
Vendor Keys:	microsoft
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;And we can check the efi variable:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;od -An -t u1 /sys/firmware/efi/efivars/SecureBoot-&amp;lt;some-long-string-of-numbers&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;If it worked, the last number should be 1.
Example output: &lt;code&gt;6 0 0 0 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;If this process didn&amp;rsquo;t work and you didn&amp;rsquo;t miss anything, the &lt;a href=&#34;https://wiki.archlinux.org/title/Unified_Extensible_Firmware_Interface/Secure_Boot&#34;&gt;wiki&lt;/a&gt; is the only hope.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Backup and Duplicate Raspberry Pi Images</title>
      <link>https://matt-w.net/blog/backup-pi-images/</link>
      <pubDate>Thu, 23 Mar 2023 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/backup-pi-images/</guid>
      <description>&lt;p&gt;Setting up a &lt;a href=&#34;../raspberry-pi-headless&#34;&gt;Raspberry Pi from scratch&lt;/a&gt; can quickly become tedious - especially if you need to configure a lot of devices in sequence.&lt;/p&gt;
&lt;p&gt;If you create backup snapshots of your Raspberry Pi images, you can quickly restore or duplicate the setup across other devices.&lt;/p&gt;
&lt;p&gt;For this example, I&amp;rsquo;ll use a Raspberry Pi that I configured with &lt;a href=&#34;../pi-zero-camera&#34;&gt;IP camera software&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The process is very straightforward.
You&amp;rsquo;ll need enough free space to backup the entire card (but we&amp;rsquo;ll fix the backup image size at the end).&lt;/p&gt;
&lt;h1 id=&#34;setup-the-micro-sd-card&#34;&gt;Setup the Micro SD Card&lt;/h1&gt;
&lt;p&gt;Remove the micro SD card from the device you want to snapshot.
Insert it into your system.&lt;/p&gt;
&lt;h2 id=&#34;identify-the-card&#34;&gt;Identify the Card&lt;/h2&gt;
&lt;p&gt;Identify the SD card using &lt;code&gt;lsblk&lt;/code&gt; or &lt;code&gt;fdisk -l&lt;/code&gt; and find the storage that matches the size of your SD card.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;~ $ lsblk
NAME              MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
sda                 8:0    0 931.5G  0 disk  
├─sda1              8:1    0   512M  0 part  
└─sda2              8:2    0   931G  0 part  
nvme1n1           259:0    0  27.3G  0 disk  
...
sdg                 8:96   1  58.2G  0 disk  
├─sdg1              8:97   1   256M  0 part  
└─sdg2              8:98   1    58G  0 part  
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My SD card is 64GB, which matches the &lt;code&gt;sdg&lt;/code&gt; device listed above.
Remember that the advertized size uses 1000 to represent a GB and the system uses 1024 to represent a GiB, which accounts for most of the 64GB/58.2GiB size discrepancy.&lt;/p&gt;
&lt;h2 id=&#34;unmount-the-card-if-necessary&#34;&gt;Unmount the Card, if Necessary&lt;/h2&gt;
&lt;p&gt;If your system auto-mounted the card, it&amp;rsquo;s best to unmount it.
This prevents data from changing on the card in the middle of backing it up, which could result in corrupted data.&lt;/p&gt;
&lt;p&gt;Unmount the device you identified:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo umount /dev/sdg*
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;The asterisk wildcard should catch the individual partitions of the card.&lt;/p&gt;
&lt;h1 id=&#34;back-up-the-image&#34;&gt;Back up the Image&lt;/h1&gt;
&lt;p&gt;Run the command below, replacing &lt;code&gt;/dev/sdg&lt;/code&gt; with the device you identified and &lt;code&gt;/home/$USER/Documents/rpi-backup.img&lt;/code&gt; with your preferred location.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo dd bs=4M \
    if=/dev/sdg \
    of=/home/$USER/Documents/rpi-backup.img \
    conv=fsync \
    status=progress
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Backing up the image typically takes a long time.
The entire disk gets backed up, despite the fact that most of it is likely empty.&lt;/p&gt;
&lt;p&gt;After the process completes, I always &lt;code&gt;chown&lt;/code&gt; the result&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo chown $USER: /home/$USER/Documents/rpi-backup.img
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&#34;shrink-the-image&#34;&gt;Shrink the Image&lt;/h1&gt;
&lt;h2 id=&#34;download-pishrink&#34;&gt;Download PiShrink&lt;/h2&gt;
&lt;p&gt;Now to use a handy repository called &lt;a href=&#34;https://github.com/Drewsif/PiShrink&#34;&gt;PiShrink&lt;/a&gt;, which abstracts away the complexity of resizing a Pi image behind &lt;a href=&#34;https://github.com/Drewsif/PiShrink/blob/master/pishrink.sh&#34;&gt;a shell script&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can either pull the repository with git:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;git clone git@github.com:Drewsif/PiShrink.git
cd PiShrink
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or if you&amp;rsquo;re more daring, &lt;code&gt;wget&lt;/code&gt; the thing and place it in your path somewhere.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh
chmod &amp;#43;x pishrink.sh
sudo mv pishrink.sh /usr/local/bin
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h2 id=&#34;shrink-ray-the-day-away&#34;&gt;Shrink Ray the Day Away&lt;/h2&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo pishrink.sh /home/$USER/Documents/rpi-backup.img
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The space savings depend on how much empty space the image has.
For me, PiShrink typically takes my Pi Images from 60 GiB down to 3 or 4 GiB, which adds up quickly if you&amp;rsquo;re saving a lot of snapshots over time.&lt;/p&gt;
&lt;h1 id=&#34;applying-the-image&#34;&gt;Applying the Image&lt;/h1&gt;
&lt;p&gt;These images can be applied the same way as any other.&lt;/p&gt;
&lt;h2 id=&#34;option-1-terminal&#34;&gt;Option 1: Terminal&lt;/h2&gt;
&lt;p&gt;In the terminal, you can apply our backup process in reverse:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo dd bs=4M \
    if=/home/$USER/Documents/rpi-backup.img \
    of=/dev/sdg \
    conv=fsync \
    status=progress
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;option-2-etcher&#34;&gt;Option 2: Etcher&lt;/h2&gt;
&lt;p&gt;As I&amp;rsquo;ve mentioned elsewhere, I like to use &lt;a href=&#34;https://www.balena.io/etcher/&#34;&gt;Etcher&lt;/a&gt; to apply images:&lt;/p&gt;

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;800px&#34; style=&#34;max-width:800px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/flashing.webm&#34; alt=&#34;Flashing an SD card with Etcher&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/flashing.ogg&#34; alt=&#34;Flashing an SD card with Etcher&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/flashing.avi&#34; alt=&#34;Flashing an SD card with Etcher&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/flashing.mov&#34; alt=&#34;Flashing an SD card with Etcher&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/flashing.mp4&#34; alt=&#34;Flashing an SD card with Etcher&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;../raspberry-pi-headless&#34;&gt;This post&lt;/a&gt; has a more detailed explanation on using Etcher, if you need it.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Raspberry Pi Webcams</title>
      <link>https://matt-w.net/blog/pi-zero-camera/</link>
      <pubDate>Tue, 22 Nov 2022 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/pi-zero-camera/</guid>
      <description>
&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/webcam-sideview.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/webcam-sideview.webp&#34; class=&#34;post-image &#34; alt=&#34;Raspberry Pi Zero Webcam&#34; width=&#34;800px&#34; style=&#34;max-width:800px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Dust not included&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s pretty easy to set up a Raspberry Pi as an independent IP camera.
It&amp;rsquo;s even easier to create a plug-and-play USB camera.
You might be able to make one too, if you can manage to find a Raspberry Pi anywhere.&lt;/p&gt;
&lt;h1 id=&#34;supplies&#34;&gt;Supplies&lt;/h1&gt;
&lt;h2 id=&#34;a-raspberry-pi&#34;&gt;A Raspberry Pi&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve only ever done this on Pi Zero v1.3&amp;rsquo;s, Pi Zero W&amp;rsquo;s, and Pi 4&amp;rsquo;s, so I don&amp;rsquo;t know about other models.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://www.adafruit.com/product/3400&#34;&gt;Pi Zero W&lt;/a&gt; (recommended)
&lt;ul&gt;
&lt;li&gt;Nominal cost: $15&lt;/li&gt;
&lt;li&gt;wifi-capable&lt;/li&gt;
&lt;li&gt;easier to find than the Pi Zero W2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.adafruit.com/product/2885&#34;&gt;Pi Zero v1.3&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Nominal cost: $10&lt;/li&gt;
&lt;li&gt;no onboard wifi&lt;/li&gt;
&lt;li&gt;fine if you&amp;rsquo;re ok with always using USB for connectivity&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.adafruit.com/product/4292&#34;&gt;Pi 4&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Nominal cost: $45&lt;/li&gt;
&lt;li&gt;wifi-capable&lt;/li&gt;
&lt;li&gt;probably overkill for a webcam&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.adafruit.com/product/5291&#34;&gt;Pi Zero W 2&lt;/a&gt; (untested)
&lt;ul&gt;
&lt;li&gt;Nominal cost: $15&lt;/li&gt;
&lt;li&gt;wifi-capable&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/pi-zero-w.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/pi-zero-w.webp&#34; class=&#34;post-image &#34; alt=&#34;Raspberry Pi Zero W&#34; width=&#34;400px&#34; style=&#34;max-width:400px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;Raspberry Pi boards are perpetually out of stock, so you may need to search across multiple suppliers using something like &lt;a href=&#34;https://rpilocator.com/&#34;&gt;rpilocator&lt;/a&gt;.
Raspberry Pi lists suppliers per-region &lt;a href=&#34;https://www.raspberrypi.com/products/raspberry-pi-zero/&#34;&gt;on their website&lt;/a&gt;, but I&amp;rsquo;ve had little luck with it.&lt;/p&gt;
&lt;h2 id=&#34;a-camera-module&#34;&gt;A Camera Module&lt;/h2&gt;
&lt;p&gt;You&amp;rsquo;ll need an actual camera.
The &lt;a href=&#34;https://www.adafruit.com/product/4561&#34;&gt;12MP Raspberry Pi High Quality Camera&lt;/a&gt; ($50) is my go-to,
but you have to buy a lens to go with it.

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/hq-camera-module.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/hq-camera-module.webp&#34; class=&#34;post-image &#34; alt=&#34;Raspberry Pi High Quality Camera Module&#34; width=&#34;400px&#34; style=&#34;max-width:400px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;
&lt;/p&gt;
&lt;p&gt;Lens-wise, I usually go for the &lt;a href=&#34;https://www.adafruit.com/product/4563&#34;&gt;6mm 3MP Wide Angle Lens for Raspberry Pi HQ Camera - 3MP&lt;/a&gt; ($25) if I don&amp;rsquo;t need a super wide-view, but more and more I&amp;rsquo;m using these &lt;a href=&#34;https://www.amazon.com/gp/product/B08GLYR572&#34;&gt;ultra-wide Arducam lenses&lt;/a&gt; ($35) so that a single camera can cover more area.&lt;/p&gt;

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/wide-angle-lens.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/wide-angle-lens.webp&#34; class=&#34;post-image &#34; alt=&#34;6mm Wide Angle Lens&#34; width=&#34;400px&#34; style=&#34;max-width:400px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;h3 id=&#34;or-go-with-budget-conscious-camera-modules&#34;&gt;Or go with budget-conscious camera modules&lt;/h3&gt;
&lt;p&gt;You can go with an &lt;a href=&#34;https://www.adafruit.com/product/3099&#34;&gt;8 megapixel camera&lt;/a&gt; ($30),
the &lt;a href=&#34;https://www.adafruit.com/product/3100&#34;&gt;NoIR version of same&lt;/a&gt; ($30) for low-light footage,
or Amazon has even &lt;a href=&#34;https://www.amazon.com/Arducam-Megapixels-Sensor-OV5647-Raspberry/dp/B012V1HEP4&#34;&gt;cheaper options&lt;/a&gt;.
Whatever works for your use-case.&lt;/p&gt;
&lt;p&gt;Heads up - these cheaper camera options will require some creativity to mount, since the mount I use is designed with the HQ camera module in mind.&lt;/p&gt;
&lt;h2 id=&#34;a-compatible-camera-cable&#34;&gt;A Compatible Camera Cable&lt;/h2&gt;
&lt;p&gt;Pi Zeros use a &lt;a href=&#34;https://www.adafruit.com/product/3157&#34;&gt;different camera cable&lt;/a&gt; that&amp;rsquo;s tapered on one end.&lt;/p&gt;

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/zero-cam-cable.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/zero-cam-cable.webp&#34; class=&#34;post-image &#34; alt=&#34;Pi Zero Raspberry Pi Camera Cable&#34; width=&#34;400px&#34; style=&#34;max-width:400px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;Make sure you get the right one for your Pi. Here is the &lt;a href=&#34;https://www.adafruit.com/product/1646&#34;&gt;standard Raspberry Pi&amp;rsquo;s cable&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/standard-cam-cable.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/standard-cam-cable.webp&#34; class=&#34;post-image &#34; alt=&#34;Standard Raspberry Pi Camera Cable&#34; width=&#34;400px&#34; style=&#34;max-width:400px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;h2 id=&#34;mounting&#34;&gt;Mounting&lt;/h2&gt;
&lt;p&gt;The HQ camera has a 1/4&amp;quot; threaded hole in its base - which is compatible with most camera mounts.&lt;/p&gt;
&lt;p&gt;To mount the camera and Raspberry Pi together,
I like the &lt;a href=&#34;https://www.adafruit.com/product/5025&#34;&gt;Zero Pro Plate for HQ Camera&lt;/a&gt;.
There&amp;rsquo;s also a &lt;a href=&#34;https://www.adafruit.com/product/5026&#34;&gt;version for the Pi 4&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;physical-port-access&#34;&gt;(Physical) Port Access&lt;/h3&gt;
&lt;p&gt;Depending on the stand you&amp;rsquo;re using,
you might need a spacer so there&amp;rsquo;s room to access the USB slot on the Pi Zero.
I&amp;rsquo;ve been using &lt;a href=&#34;https://www.amazon.com/gp/product/B07HKZMC1T&#34;&gt;this $10 camera adapter kit&lt;/a&gt; for years and it did the job fine for me.&lt;/p&gt;
&lt;h1 id=&#34;putting-it-together&#34;&gt;Putting it together&lt;/h1&gt;
&lt;p&gt;Assembly varies based on the components you selected, but is generally straightforward.&lt;/p&gt;

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/stills_1.1.2.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/stills_1.1.2.webp&#34; class=&#34;post-image &#34; alt=&#34;Pi Zero Webcam Mount Parts&#34; width=&#34;800px&#34; style=&#34;max-width:800px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;Once assembled, you can easily throw everything on a little &lt;a href=&#34;https://www.amazon.com/dp/B08SBQNB1V&#34;&gt;tripod stand&lt;/a&gt; and call it a day.&lt;/p&gt;

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/IMG_3108.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/IMG_3108.webp&#34; class=&#34;post-image &#34; alt=&#34;Raspberry Pi Zero Webcam&#34; width=&#34;800px&#34; style=&#34;max-width:800px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;Software setup completely depends on what type of camera you want to use.
Let&amp;rsquo;s start with the simpler of the 2 options.&lt;/p&gt;
&lt;h1 id=&#34;setup-option-1---plug-and-play-usb-camera&#34;&gt;Setup Option 1 - Plug-and-Play USB Camera&lt;/h1&gt;
&lt;p&gt;There are many different methods floating around, but the only software that worked for me is &lt;a href=&#34;https://github.com/showmewebcam/showmewebcam&#34;&gt;showmewebcam&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;download-the-image&#34;&gt;Download the image&lt;/h2&gt;
&lt;p&gt;Download the appropriate image from &lt;a href=&#34;https://github.com/showmewebcam/showmewebcam/releases&#34;&gt;https://github.com/showmewebcam/showmewebcam/releases&lt;/a&gt;.
The release files are named based on the Raspberry Pi they&amp;rsquo;re intented to work with.
At time of writing, that means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/showmewebcam/showmewebcam/releases/download/v1.91/sdcard-raspberrypi0-8e9f9ac.img.zip&#34;&gt;sdcard-raspberrypi0-8e9f9ac.img.zip&lt;/a&gt; for the Pi Zero v1.3&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/showmewebcam/showmewebcam/releases/download/v1.91/sdcard-raspberrypi0w-8e9f9ac.img.zip&#34;&gt;sdcard-raspberrypi0w-8e9f9ac.img.zip&lt;/a&gt; for the Pi Zero W&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/showmewebcam/showmewebcam/releases/download/v1.91/sdcard-raspberrypi4-8e9f9ac.img.zip&#34;&gt;sdcard-raspberrypi4-8e9f9ac.img.zip&lt;/a&gt; for the Pi 4&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unzip the image for the next step.&lt;/p&gt;
&lt;h2 id=&#34;image-burning-software&#34;&gt;Image Burning Software&lt;/h2&gt;
&lt;p&gt;I prefer balenaEtcher to actually burn images.&lt;/p&gt;
&lt;p&gt;Download balenaEtcher from the &lt;a href=&#34;https://www.balena.io/etcher/&#34;&gt;official site&lt;/a&gt;.
Windows and OSX have their own installers.
On linux, you can just unzip and run the AppImage.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;unzip balena-etcher-electron-1.7.9-linux-x64.zip
./balenaEtcher-1.7.9-x64.AppImage
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;insert a micro SD card&lt;/li&gt;
&lt;li&gt;launch Etcher&lt;/li&gt;
&lt;li&gt;select the &lt;code&gt;.img&lt;/code&gt; file you downloaded&lt;/li&gt;
&lt;li&gt;select the micro SD card you inserted&lt;/li&gt;
&lt;li&gt;burn the image.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Etcher is usually able to guess pretty well which is the SD card you probably want, but be careful not to pick the wrong volume.&lt;/p&gt;

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;800px&#34; style=&#34;max-width:800px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/flashing.webm&#34; alt=&#34;Flashing an SD card with Etcher&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/flashing.ogg&#34; alt=&#34;Flashing an SD card with Etcher&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/flashing.avi&#34; alt=&#34;Flashing an SD card with Etcher&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/flashing.mov&#34; alt=&#34;Flashing an SD card with Etcher&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/flashing.mp4&#34; alt=&#34;Flashing an SD card with Etcher&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;p&gt;With everything burned onto the micro SD card, you just have to insert the card into the Pi camera.&lt;/p&gt;
&lt;h2 id=&#34;plug-and-play&#34;&gt;Plug and Play&lt;/h2&gt;
&lt;p&gt;Now the camera should be completely ready.
Just plug it into your system using a USB cable.
The Pi&amp;rsquo;s build-in LED should blink 3 times to indicate it&amp;rsquo;s ready.
It should come up as a typical web camera.&lt;/p&gt;
&lt;p&gt;The project&amp;rsquo;s README has you covered if you need to &lt;a href=&#34;https://github.com/showmewebcam/showmewebcam#debugging&#34;&gt;debug&lt;/a&gt; or to &lt;a href=&#34;https://github.com/showmewebcam/showmewebcam#customizing-camera-settings&#34;&gt;change the camera settings&lt;/a&gt;.&lt;/p&gt;
&lt;h1 id=&#34;setup-option-2---ip-camera&#34;&gt;Setup Option 2 - IP Camera&lt;/h1&gt;
&lt;p&gt;The best IP interface I&amp;rsquo;ve found (and admittedly, the only one I&amp;rsquo;ve used much) is &lt;a href=&#34;https://github.com/silvanmelchior/RPi_Cam_Web_Interface&#34;&gt;RPi-Cam-Web-Interface&lt;/a&gt;.
This is how most of my cameras are configured.&lt;/p&gt;

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;800px&#34; style=&#34;max-width:800px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/rpi-cam-timelapse.webm&#34; alt=&#34;Running RPi-Cam-Web-Interface in timelapse mode to record a Bambu Labs P1P 3d printer&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/rpi-cam-timelapse.ogg&#34; alt=&#34;Running RPi-Cam-Web-Interface in timelapse mode to record a Bambu Labs P1P 3d printer&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/rpi-cam-timelapse.avi&#34; alt=&#34;Running RPi-Cam-Web-Interface in timelapse mode to record a Bambu Labs P1P 3d printer&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/rpi-cam-timelapse.mov&#34; alt=&#34;Running RPi-Cam-Web-Interface in timelapse mode to record a Bambu Labs P1P 3d printer&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/rpi-cam-timelapse.mp4&#34; alt=&#34;Running RPi-Cam-Web-Interface in timelapse mode to record a Bambu Labs P1P 3d printer&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;p&gt;With this setup, the camera runs as its own host on your network.
You can connect to it from your browser to take videos and images.
You can even do time lapses.&lt;/p&gt;
&lt;p&gt;I find the ability to capture video based on motion detection invaluable.&lt;/p&gt;
&lt;h2 id=&#34;install-raspberry-pi-os-on-a-micro-sd-card&#34;&gt;Install Raspberry Pi OS on a Micro SD Card&lt;/h2&gt;
&lt;p&gt;Follow the steps on how to setup headless Raspberry PI &lt;a href=&#34;../raspberry-pi-headless&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After Raspberry Pi OS is installed and you have SSH access to the system, continue below.&lt;/p&gt;
&lt;h2 id=&#34;install-rpi-cam-web-interface&#34;&gt;Install RPi-Cam-Web-Interface&lt;/h2&gt;
&lt;p&gt;Boot up the Raspberry Pi and connect over SSH. For me:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;ssh matt@raspberrypi.local
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;If you haven&amp;rsquo;t already, run yer updates:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo apt-get update
sudo apt-get dist-upgrade
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Install git, which doesn&amp;rsquo;t come standard on Raspberry Pi OS Lite:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo apt-get install git
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s critical to enable the legacy camera interface for the software to work.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo raspi-config
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Navigate to Interface Options &amp;gt; Legacy Camera and select &amp;ldquo;Yes&amp;rdquo; to enable the interface.&lt;/p&gt;

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;800px&#34; style=&#34;max-width:800px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/ip-2-enable-legacy-cam.webm&#34; alt=&#34;Enabling the Legacy Camera interface on the Raspberry Pi&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/ip-2-enable-legacy-cam.ogg&#34; alt=&#34;Enabling the Legacy Camera interface on the Raspberry Pi&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/ip-2-enable-legacy-cam.avi&#34; alt=&#34;Enabling the Legacy Camera interface on the Raspberry Pi&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/ip-2-enable-legacy-cam.mov&#34; alt=&#34;Enabling the Legacy Camera interface on the Raspberry Pi&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/ip-2-enable-legacy-cam.mp4&#34; alt=&#34;Enabling the Legacy Camera interface on the Raspberry Pi&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;p&gt;Now we can download and install the repo:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;git clone https://github.com/silvanmelchior/RPi_Cam_Web_Interface.git
cd RPi_Cam_Web_Interface
./install.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I typically just use the defaults, but there are different options in terms of webservers and authentication, if you want it.&lt;/p&gt;
&lt;p&gt;The installation takes 10-15 minutes on a Pi Zero W.&lt;/p&gt;

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;800px&#34; style=&#34;max-width:800px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/ip-3-install-repo.webm&#34; alt=&#34;Installing the RPi Cam Web Interface repository on the Raspberry Pi&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/ip-3-install-repo.ogg&#34; alt=&#34;Installing the RPi Cam Web Interface repository on the Raspberry Pi&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/ip-3-install-repo.avi&#34; alt=&#34;Installing the RPi Cam Web Interface repository on the Raspberry Pi&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/ip-3-install-repo.mov&#34; alt=&#34;Installing the RPi Cam Web Interface repository on the Raspberry Pi&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/ip-3-install-repo.mp4&#34; alt=&#34;Installing the RPi Cam Web Interface repository on the Raspberry Pi&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;p&gt;The scripts in the RPi_Cam_Web_Interface folder do pretty much what you&amp;rsquo;d expect.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;cam.sh
debug.sh
install.sh
remove.sh
start.sh
stop.sh
update.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;./start.sh&lt;/code&gt; if it&amp;rsquo;s not started already.&lt;/p&gt;
&lt;p&gt;Now you can navigate to &lt;code&gt;http://raspberrypi.local/html/&lt;/code&gt; to see the interface.
If you have a different host set for your raspberrypi, use that host or the IP address, instead.&lt;/p&gt;
&lt;p&gt;
&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;800px&#34; style=&#34;max-width:800px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/ip-4-demo.webm&#34; alt=&#34;The IP camera interface after installation&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/ip-4-demo.ogg&#34; alt=&#34;The IP camera interface after installation&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/ip-4-demo.avi&#34; alt=&#34;The IP camera interface after installation&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/ip-4-demo.mov&#34; alt=&#34;The IP camera interface after installation&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/pi-zero-camera/ip-4-demo.mp4&#34; alt=&#34;The IP camera interface after installation&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;em&gt;the purple artifacts are from my hand nudging the camera&amp;rsquo;s ribbon cable&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now go crazy.
The wiki has a lot more in-depth information on configuration and different setups, if you want more: &lt;a href=&#34;https://elinux.org/RPi-Cam-Web-Interface&#34;&gt;https://elinux.org/RPi-Cam-Web-Interface&lt;/a&gt;&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Raspberry Pi Headless on Bullseye</title>
      <link>https://matt-w.net/blog/raspberry-pi-headless/</link>
      <pubDate>Tue, 23 Aug 2022 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/raspberry-pi-headless/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been on a crusade to upgrade my half dozen Raspberry Pi&amp;rsquo;s to the most recent Raspberry Pi OS version.
It&amp;rsquo;s more of a pain to setup headless than it used to be.
Here&amp;rsquo;s my notes on the process.&lt;/p&gt;
&lt;p&gt;All this assumes you&amp;rsquo;ll be using either wifi or ethernet with your Raspberry Pi.&lt;/p&gt;
&lt;h1 id=&#34;what-to-download&#34;&gt;What to Download&lt;/h1&gt;
&lt;h2 id=&#34;raspberry-pi-os-lite&#34;&gt;Raspberry Pi OS Lite&lt;/h2&gt;
&lt;p&gt;Download one of the &lt;strong&gt;Raspberry Pi OS Lite&lt;/strong&gt; images on the &lt;a href=&#34;https://www.raspberrypi.com/software/operating-systems/&#34;&gt;official downloads page&lt;/a&gt;.
Only download the 64-bit version if your Raspberry Pi is compatible (e.g. Pi Zero W&amp;rsquo;s and Pi Zero v1.3&amp;rsquo;s don&amp;rsquo;t work with 64 bit).&lt;/p&gt;

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/rpi-64-compatibility.resized.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/rpi-64-compatibility.resized.jpg&#34; class=&#34;post-image &#34; alt=&#34;Compatibility for Raspberry Pi OS Lite&#34; width=&#34;400px&#34; style=&#34;max-width:400px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;You may need to decompress the OS image with &lt;code&gt;xz&lt;/code&gt;.
If you don&amp;rsquo;t already have xz, you can install it via:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;# Arch
sudo pacman -Sy xz

# Debian
sudo apt install xz-utils
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Decompressing:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;xz -d 2022-04-04-raspios-bullseye-armhf-lite.img.xz
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h2 id=&#34;image-burning-software&#34;&gt;Image Burning Software&lt;/h2&gt;
&lt;p&gt;I prefer balenaEtcher to actually burn the image.&lt;/p&gt;
&lt;p&gt;Download balenaEtcher from the &lt;a href=&#34;https://www.balena.io/etcher/&#34;&gt;official site&lt;/a&gt;.
Windows and OSX have their own installers.
On linux, you can just unzip and run the AppImage.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;unzip balena-etcher-electron-1.7.9-linux-x64.zip
./balenaEtcher-1.7.9-x64.AppImage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With Etcher and the Pi OS image at hand, insert a micro SD card and burn the image.&lt;/p&gt;
&lt;p&gt;
&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;800px&#34; style=&#34;max-width:800px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/flashing.webm&#34; alt=&#34;Flashing an SD card with Etcher&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/flashing.ogg&#34; alt=&#34;Flashing an SD card with Etcher&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/flashing.avi&#34; alt=&#34;Flashing an SD card with Etcher&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/flashing.mov&#34; alt=&#34;Flashing an SD card with Etcher&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/flashing.mp4&#34; alt=&#34;Flashing an SD card with Etcher&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;em&gt;It&amp;rsquo;s not generally this fast.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Remember which device it is so you can mount it later;
for me, it&amp;rsquo;s &lt;code&gt;/dev/sdf&lt;/code&gt;.
Etcher is usually able to guess pretty well which devices you probably want, but be careful not to pick the wrong one.&lt;/p&gt;
&lt;h1 id=&#34;mount-the-micro-sd-card&#34;&gt;Mount the Micro SD Card&lt;/h1&gt;
&lt;p&gt;If your system automatically mounts devices, you can probably just unplug and replug the card after flashing.
You&amp;rsquo;ll need to update the paths in the rest of this guide if you go with that.&lt;/p&gt;
&lt;p&gt;You can use &lt;code&gt;lsblk&lt;/code&gt; to look at the currently mounted devices, where they&amp;rsquo;re mounted, and other info.
This can help you narrow down which device or path you&amp;rsquo;re looking for.&lt;/p&gt;

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;1000px&#34; style=&#34;max-width:1000px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/lsblk.webm&#34; alt=&#34;My micro SD card and the mount paths in lsblk&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/lsblk.ogg&#34; alt=&#34;My micro SD card and the mount paths in lsblk&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/lsblk.avi&#34; alt=&#34;My micro SD card and the mount paths in lsblk&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/lsblk.mov&#34; alt=&#34;My micro SD card and the mount paths in lsblk&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/lsblk.mp4&#34; alt=&#34;My micro SD card and the mount paths in lsblk&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;p&gt;I&amp;rsquo;m using &lt;code&gt;/dev/sdf&lt;/code&gt;, as mentioned above.
To manually mount:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;# Make the mount point
sudo mkdir -p /media/microsd/boot

# Mount the partition
sudo mount /dev/sdf1 /media/microsd/boot/
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re only mounting &lt;code&gt;boot&lt;/code&gt; because it&amp;rsquo;s the only partition we need.&lt;/p&gt;
&lt;h1 id=&#34;setup-wifi&#34;&gt;Setup Wifi&lt;/h1&gt;
&lt;p&gt;This is only necessary if you aren&amp;rsquo;t using ethernet for your Pi.&lt;/p&gt;
&lt;p&gt;Create a &lt;code&gt;wpa_supplicant.conf&lt;/code&gt; file in the boot drive.
Use &lt;code&gt;sudo&lt;/code&gt; and your favorite editor to create &lt;code&gt;/media/microsd/boot/wpa_supplicant.conf&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;country=US
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1

network={
       ssid=&amp;#34;YOUR WIFI&amp;#39;S NAME HERE&amp;#34;
       psk=&amp;#34;YOUR WIFI&amp;#39;S PASSWORD HERE&amp;#34;
       key_mgmt=WPA-PSK
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update your network info and country as-needed.&lt;/p&gt;
&lt;h1 id=&#34;add-a-user&#34;&gt;Add a User&lt;/h1&gt;
&lt;p&gt;Since Bullseye, the default &lt;code&gt;pi&lt;/code&gt; user is no longer available.
For Raspberry Pi OS desktop, it&amp;rsquo;ll force you to make a user on first boot;
with headless that&amp;rsquo;s not an option.&lt;/p&gt;
&lt;p&gt;To setup a user that you can login with, we need to add one to &lt;code&gt;boot/userconf&lt;/code&gt;.
Note the spots for your desired password and desired username.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo sh -c &amp;#39;echo &amp;#34;YOUR-PASSWORD&amp;#34; | \
openssl passwd -6 -stdin | \
xargs -I {} echo &amp;#34;YOUR-USERNAME:{}&amp;#34; &amp;gt; /media/microsd/boot/userconf&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&#34;enable-ssh-access&#34;&gt;Enable SSH Access&lt;/h1&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo touch /media/microsd/boot/ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;ssh-over-usb-configuration-optional&#34;&gt;SSH Over USB Configuration (Optional)&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Source: &lt;a href=&#34;https://forums.raspberrypi.com/viewtopic.php?p=2014934&amp;amp;hilit=USB+SSH#p2014934&#34;&gt;Getting WIFI and USB serial port to work on Pi0W&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Open the command line config file &lt;code&gt;/media/microsd/boot/cmdline.txt&lt;/code&gt; and add:
&lt;code&gt;modules-load=dwc2,g_cdc&lt;/code&gt; after &lt;code&gt;rootwait&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Append to the config file:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo sh -c &amp;#39;echo &amp;#34;dtoverlay=dwc2,dr_mode=peripheral&amp;#34; &amp;gt;&amp;gt; /media/microsd/boot/config.txt&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;The USB connection won&amp;rsquo;t work until the gadget service is running.
After your first boot, you&amp;rsquo;ll need to enable login via the gadget serial port (covered below).&lt;/p&gt;
&lt;h1 id=&#34;unmount&#34;&gt;Unmount&lt;/h1&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo umount /media/microsd/boot
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&#34;first-boot&#34;&gt;First Boot&lt;/h1&gt;
&lt;p&gt;Now you can remove the micro SD card from your system and place it in the Raspberry Pi.
If you wanted to use ethernet, plug that in.
Turn on the system and wait for it to boot.&lt;/p&gt;
&lt;p&gt;SSH into the pi using the credentials you setup in userconf.
Most of the time you can use &lt;code&gt;raspberrypi.local&lt;/code&gt; to access the Pi.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;# Clear out any old host information
ssh-keygen -R raspberrypi.local

# Connect to the default hostname
ssh YOUR-USERNAME@raspberrypi.local
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With a successful connection,
we can run updates on the Pi, itself.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo apt update
sudo apt full-upgrade
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And you can run through &lt;code&gt;raspi-config&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo raspi-config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The configurations I always update are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;System &amp;gt; Hostname&lt;/li&gt;
&lt;li&gt;Localisation &amp;gt; Locale&lt;/li&gt;
&lt;li&gt;Localisation &amp;gt; Timezone&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;
&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;1000px&#34; style=&#34;max-width:1000px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/raspi-config-short.webm&#34; alt=&#34;Setting hostname, locale, and timezone through raspi-config&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/raspi-config-short.ogg&#34; alt=&#34;Setting hostname, locale, and timezone through raspi-config&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/raspi-config-short.avi&#34; alt=&#34;Setting hostname, locale, and timezone through raspi-config&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/raspi-config-short.mov&#34; alt=&#34;Setting hostname, locale, and timezone through raspi-config&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/raspi-config-short.mp4&#34; alt=&#34;Setting hostname, locale, and timezone through raspi-config&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;em&gt;Loading times have been cut out - locale takes awhile to generate.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;After reboot, you should be in a pretty good state.
Most people can stop here.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;If you updated the hostname, you&amp;rsquo;ll need to use &lt;code&gt;your-hostname.local&lt;/code&gt; instead of &lt;code&gt;raspberrypi.local&lt;/code&gt; for the remainder of ssh commands.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;enable-ssh-over-usb-service-optional&#34;&gt;Enable SSH Over USB Service (Optional)&lt;/h2&gt;
&lt;p&gt;If you configured SSH over USB, the connection won&amp;rsquo;t work until the gadget service is running.
After your first boot, enable login via the gadget serial port on the Raspberry Pi:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo systemctl enable --now getty@ttyGS0.service
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;When you connect the Raspberry Pi over USB, usually there&amp;rsquo;s a specific USB port you have to use.&lt;/p&gt;

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/usb-port.resized.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/usb-port.resized.jpg&#34; class=&#34;post-image &#34; alt=&#34;USB Port on a Raspbery Pi Zero W&#34; width=&#34;800px&#34; style=&#34;max-width:800px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;h2 id=&#34;ssh-hardening-and-uploading-keys-optional&#34;&gt;SSH Hardening and Uploading Keys (Optional)&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Unless you really care about security, you can skip this.
If you lose your key, you lose your access to the Pi&amp;rsquo;s SSH interface.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s best to avoid password authentication over SSH and opt for public key authentication, instead.&lt;/p&gt;
&lt;p&gt;If you don&amp;rsquo;t have an existing key, you can generate one (on your &lt;em&gt;local&lt;/em&gt; system) as follows:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;# If you choose to enter a password, you&amp;#39;ll be prompted for it on each connection
ssh-keygen -t rsa -b 4096 -C &amp;#34;your-email&amp;#34; -f ~/.ssh/your-key-name.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Uploading an SSH key to the Raspberry Pi:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;ssh-copy-id -i ~/.ssh/your-key-name.pem YOUR-USERNAME@raspberrypi.local
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t be afraid to use &lt;code&gt;ssh-copy-id -f&lt;/code&gt; to force it if &lt;code&gt;ssh-copy-id&lt;/code&gt; thinks the key is already there.&lt;/p&gt;
&lt;p&gt;Test that your key works:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;ssh -i ~/.ssh/your-key-name.pem YOUR-USERNAME@raspberrypi.local
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should &lt;strong&gt;NOT&lt;/strong&gt; be prompted for the Raspberry Pi&amp;rsquo;s password unless there&amp;rsquo;s a problem with the key setup.
If you set an SSH key password, you will be prompted for that.&lt;/p&gt;
&lt;p&gt;Only proceed if the key is working.
It&amp;rsquo;s better to use password authentication than to lock yourself out.&lt;/p&gt;
&lt;p&gt;If the key&amp;rsquo;s working, you can make SSH-ing easier by adding your Pi&amp;rsquo;s info to &lt;code&gt;~/.ssh/config&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;Host your-hostname
  IdentityFile ~/.ssh/your-key-name.pem
  User your-username
  Hostname your-hostname.local
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you should be able to connect just by running &lt;code&gt;ssh your-hostname&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&#34;basic-hardening&#34;&gt;Basic Hardening&lt;/h3&gt;
&lt;p&gt;Open the sshd config with some terminal editor:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo nano /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Most of the settings we&amp;rsquo;re looking for are already in the sshd config;
many of them are commented out.
Here are the key settings I usually update/add:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;AllowUsers YOUR-USERNAME
LoginGraceTime 1m
PermitRootLogin no
StrictModes yes
PasswordAuthentication no
PermitEmptyPasswords no
UsePAM no
Protocol 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now restart sshd on the Raspberry Pi to pick up the new settings.&lt;/p&gt;
&lt;p&gt;
&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;sudo systemctl restart sshd
&lt;/code&gt;&lt;/pre&gt;
&lt;em&gt;Leave your existing SSH session open until you&amp;rsquo;ve tested that you can still connect. If something goes wrong, you can roll back the changes with the existing session.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You should no longer be able to login to the Raspberry Pi using a password;
the key you generated should be the only way to do it.&lt;/p&gt;

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/ssh-key-only.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/ssh-key-only.png&#34; class=&#34;post-image &#34; alt=&#34;The new SSH settings rejecting a &#34; width=&#34;600px&#34; style=&#34;max-width:600px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;h1 id=&#34;debugging-issues-with-setup&#34;&gt;Debugging Issues With Setup&lt;/h1&gt;
&lt;h2 id=&#34;the-raspberry-pi-gives-a-repeating-led-blink-sequence&#34;&gt;The Raspberry Pi gives a repeating LED blink sequence.&lt;/h2&gt;
&lt;p&gt;Check out the &lt;a href=&#34;https://www.raspberrypi.com/documentation/computers/configuration.html#led-warning-flash-codes&#34;&gt;LED blink error codes&lt;/a&gt; for Raspberry Pi&amp;rsquo;s to help debug the issue.&lt;/p&gt;
&lt;p&gt;For example, my Pi Zero W was giving 7 short blinks over and over;
it couldn&amp;rsquo;t detect the kernel because I&amp;rsquo;d accidentally used the incompatible 64-bit version of Raspberry Pi OS Lite.&lt;/p&gt;
&lt;h2 id=&#34;ssh-could-not-resolve-hostname-name-or-service-not-known&#34;&gt;ssh: Could not resolve hostname: Name or service not known&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1st check:&lt;/strong&gt; Is your Raspberry Pi properly booted or is it exhibiting a blink sequence?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2nd check:&lt;/strong&gt; Is your Raspberry Pi connected to your network?
It&amp;rsquo;s worth double checking your ethernet cable or the &lt;code&gt;wpa_supplicant.conf&lt;/code&gt; file from earlier.&lt;/p&gt;
&lt;h3 id=&#34;scanning-for-the-raspberry-pi&#34;&gt;Scanning for the Raspberry Pi&lt;/h3&gt;
&lt;p&gt;If your Pi is booted and connected, your system may be unable to resolve &lt;code&gt;raspberrypi.local&lt;/code&gt;.
You can use &lt;code&gt;nmap&lt;/code&gt; to find it manually.&lt;/p&gt;
&lt;p&gt;This will look for devices with port 22 open and provide the SSH version.
As of writing, Raspberry Pi OS should have &lt;code&gt;Raspbian&lt;/code&gt; in the SSH version name, so it&amp;rsquo;s easy to filter on.

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;# Scan for open SSH ports
nmap 192.168.1.0/24 -p22 --open -sV | \
# Filter it down to &amp;#39;Raspbian&amp;#39; results
grep Raspbian -B 4
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Example output:&lt;/p&gt;

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/nmap-scan.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2022/rpi-headless-bullseye/nmap-scan.webp&#34; class=&#34;post-image &#34; alt=&#34;Scanning for the Raspberry Pi with nmap&#34; width=&#34;600px&#34; style=&#34;max-width:600px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;You probably won&amp;rsquo;t have this many results;
ideally you&amp;rsquo;ll just have 1.&lt;/p&gt;
&lt;p&gt;Try out these IP addresses with SSH.
In my case, I needed &lt;code&gt;192.168.1.134&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;ssh matt@192.168.1.134
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can use the IP address in place of the hostname.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>CAD With Code: First 3D Models in OpenSCAD</title>
      <link>https://matt-w.net/blog/start-openscad/</link>
      <pubDate>Thu, 28 Jul 2022 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/start-openscad/</guid>
      <description>&lt;p&gt;CAD modeling never felt very intuitive to me.
It still doesn&amp;rsquo;t.
Reducing complex geometries to a set of basic shapes and transformations is hard.
Finding a good workflow can help a lot.&lt;/p&gt;
&lt;h1 id=&#34;openscad&#34;&gt;OpenSCAD&lt;/h1&gt;
&lt;p&gt;Autodesk&amp;rsquo;s Fusion 360 is the de facto standard that seemingly every &amp;lsquo;maker&amp;rsquo; uses.
Most alternatives work in more or less the same way.
But then there&amp;rsquo;s OpenSCAD, which ditches the UI elements in favor of raw code.
The syntax is basically Go and Python thrown into a blender.&lt;/p&gt;

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2022/start-openscad/openscad.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2022/start-openscad/openscad.png&#34; class=&#34;post-image &#34; alt=&#34;OpenSCAD in action&#34; width=&#34;800px&#34; style=&#34;max-width:800px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;Despite knowing about OpenSCAD for awhile, I kept putting off working with it.
I wanted to have the conveniences that Fusion 360 gives you out of the box.
Niceties like chamfers, bevels, and smoothing can make a huge difference in a model.&lt;/p&gt;
&lt;p&gt;My fears were misguided.&lt;/p&gt;
&lt;h2 id=&#34;belfry-openscad-library&#34;&gt;Belfry OpenSCAD Library&lt;/h2&gt;
&lt;p&gt;The Belfry OpenSCAD Library (&lt;a href=&#34;https://github.com/revarbat/BOSL2&#34;&gt;BOSL2&lt;/a&gt;) has support for most of the features I thought I would miss from Fusion 360.
Plus, the quality of its wiki is extraordinary - stuffed with great examples.&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;ll be relying on BOSL2 for the foreseeable future.&lt;/p&gt;

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;800px&#34; style=&#34;max-width:800px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/bosl2-wiki-tutorial.webm&#34; alt=&#34;BOSL2 Wiki Tutorials&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/bosl2-wiki-tutorial.ogg&#34; alt=&#34;BOSL2 Wiki Tutorials&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/bosl2-wiki-tutorial.avi&#34; alt=&#34;BOSL2 Wiki Tutorials&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/bosl2-wiki-tutorial.mov&#34; alt=&#34;BOSL2 Wiki Tutorials&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/bosl2-wiki-tutorial.mp4&#34; alt=&#34;BOSL2 Wiki Tutorials&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;h1 id=&#34;first-steps&#34;&gt;First Steps&lt;/h1&gt;
&lt;p&gt;The &lt;a href=&#34;https://files.openscad.org/tutorial/&#34;&gt;official tutorial&lt;/a&gt; for OpenSCAD is quite good. I went through about half of it before getting bored and bailing.&lt;/p&gt;
&lt;p&gt;I want to learn OpenSCAD and reduce the pain of future modeling at the same time.
How?
Most of the models I&amp;rsquo;ve made involved screw holes - and screw holes are about as easy as CAD can get, so that&amp;rsquo;s where I started.&lt;/p&gt;
&lt;p&gt;I want to make a jig so that I can consistently create holes that standard metric screws can cut their threads into.&lt;/p&gt;
&lt;p&gt;As a starting point for screw sizes, I went off the specs.
m2 screws are supposed to be 2 millimeters; m3 are supposed to be 3mm and so on.
(But if you&amp;rsquo;re going to do 3d modeling, a set of &lt;a href=&#34;https://www.amazon.com/gp/product/B07VSVMWTJ&#34;&gt;calipers&lt;/a&gt; is an absolute must.)&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s what I worked out for a very basic test model.&lt;/p&gt;
&lt;p&gt;
&lt;pre&gt;&lt;code  class=&#34;language-javascript&#34;&gt;include &amp;lt;BOSL2/std.scad&amp;gt;;

// for metric screws to self-cut threads in the 3d prints
module screwTest() {

    // All of these are in mm.
    // I just fiddled with them until it looked alright.
    depth = 5;
    height = 7;
    width = 55;

    // subtract the screw holes from the base
    difference() {

        // the base jig to contain all of the screw holes
        cuboid([width,height,depth]);

        // m2
        left(width * 0.35)
            cyl(d=2, h=depth);
        // m2.5
        left(width * 0.12)
            cyl(d=2.5, h=depth);
        // m3
        right(width * 0.05)
            cyl(d=3, h=depth);
        // m4
        right(width * 0.23)
            cyl(d=4, h=depth);
        // m5
        right(width * 0.43)
            cyl(d=5, h=depth);
    }
}

screwTest();
&lt;/code&gt;&lt;/pre&gt;
&lt;em&gt;units are all in millimeters&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;600px&#34; style=&#34;max-width:600px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-selfcut-bare.webm&#34; alt=&#34;Basic jig to test self-cutting screw sizes&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-selfcut-bare.ogg&#34; alt=&#34;Basic jig to test self-cutting screw sizes&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-selfcut-bare.avi&#34; alt=&#34;Basic jig to test self-cutting screw sizes&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-selfcut-bare.mov&#34; alt=&#34;Basic jig to test self-cutting screw sizes&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-selfcut-bare.mp4&#34; alt=&#34;Basic jig to test self-cutting screw sizes&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;p&gt;This gave me a decent jig, but we can make it look a lot cleaner and add labels.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-javascript&#34;&gt;include &amp;lt;BOSL2/std.scad&amp;gt;;

// for metric screws to self-cut threads
module screwTest() {
    depth = 5;
    height = 7;
    width = 55;

    difference() {
        cuboid(
	    [width,height,depth],
	    rounding=3, // smooths out the edges
	    edges=&amp;#34;Z&amp;#34;   // leaves the top and bottom flat (for cleaner 3d printing)
	);

        // m2
        left(width * 0.35)union(){
            cyl(d=2, h=depth);

            up(depth * 0.5 - 1)
            fwd(1)
            left(7)
            text3d(
                &amp;#34;m2&amp;#34;,
                size=3
            );
        }
        // m2.5
        left(width * 0.12)union(){
            cyl(d=2.5, h=depth);

            up(depth * 0.5 - 1)
            fwd(1)
            left(10.5)
            text3d(
                &amp;#34;m2.5&amp;#34;,
                size=3
            );
        }
        // m3
        right(width * 0.05)union(){
            cyl(d=3, h=depth);

            up(depth * 0.5 - 1)
            fwd(1)
            left(7.5)
            text3d(
                &amp;#34;m3&amp;#34;,
                size=3
            );
        }
        // m4
        right(width * 0.23)union(){
            cyl(d=4, h=depth);

            up(depth * 0.5 - 1)
            fwd(1)
            left(8)
            text3d(
                &amp;#34;m4&amp;#34;,
                size=3
            );
        }
        // m5
        right(width * 0.43)union(){
            cyl(d=5, h=depth);

            up(depth * 0.5 - 1)
            fwd(1)
            left(8.5)
            text3d(
                &amp;#34;m5&amp;#34;,
                size=3
            );
        }
    }
}

screwTest();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So much better.&lt;/p&gt;

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;600px&#34; style=&#34;max-width:600px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-selfcut.webm&#34; alt=&#34;Jig to test self-cutting screw sizes&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-selfcut.ogg&#34; alt=&#34;Jig to test self-cutting screw sizes&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-selfcut.avi&#34; alt=&#34;Jig to test self-cutting screw sizes&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-selfcut.mov&#34; alt=&#34;Jig to test self-cutting screw sizes&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-selfcut.mp4&#34; alt=&#34;Jig to test self-cutting screw sizes&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;p&gt;With that as a base, I created additional jigs.
One to size out how to make slots for captive nuts.
One to size out these &lt;a href=&#34;https://www.amazon.com/gp/product/B07VFZWWXY?psc=1&#34;&gt;press-fit inserts&lt;/a&gt; I bought and never used.&lt;/p&gt;
&lt;p&gt;Captive nuts are when you put a nut inside the plastic itself and screw into it.
Since nuts are usually much stronger than 3d printed plastic, using a captive nut can really increase the strength of a model.&lt;/p&gt;

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;600px&#34; style=&#34;max-width:600px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-captive.webm&#34; alt=&#34;Jig to test screw captive nut sizes&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-captive.ogg&#34; alt=&#34;Jig to test screw captive nut sizes&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-captive.avi&#34; alt=&#34;Jig to test screw captive nut sizes&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-captive.mov&#34; alt=&#34;Jig to test screw captive nut sizes&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-captive.mp4&#34; alt=&#34;Jig to test screw captive nut sizes&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;p&gt;Press-fit inserts are female-threaded pieces of metal that you heat up and push into the plastic. The idea is that the plastic will melt around the insert and hold it tight after cooling.

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;600px&#34; style=&#34;max-width:600px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-inserts.webm&#34; alt=&#34;Jig to test press-fit inserts&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-inserts.ogg&#34; alt=&#34;Jig to test press-fit inserts&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-inserts.avi&#34; alt=&#34;Jig to test press-fit inserts&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-inserts.mov&#34; alt=&#34;Jig to test press-fit inserts&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-inserts.mp4&#34; alt=&#34;Jig to test press-fit inserts&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;
&lt;/p&gt;
&lt;h1 id=&#34;iteration&#34;&gt;Iteration&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;The current versions of any of these: &lt;a href=&#34;https://github.com/matt-fff/scadlib&#34;&gt;https://github.com/matt-fff/scadlib&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;With these basic jigs set up, I could actually test them on the 3d printer.&lt;/p&gt;
&lt;p&gt;So comes the iteration loop:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Print the jig.&lt;/li&gt;
&lt;li&gt;Test the hole sizes.&lt;/li&gt;
&lt;li&gt;Tweak the SCAD file.&lt;/li&gt;
&lt;li&gt;Repeat.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of the jig holes need to be large enough that they&amp;rsquo;re easy to use, but none
of the holes can be too large.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Press-fit insert jig:&lt;/strong&gt;
The holes need to be small enough that the plastic will fully conform to the insert after it cools.&lt;/p&gt;

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;400px&#34; style=&#34;max-width:400px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/press-fit-soldering.webm&#34; alt=&#34;Soldering the press-fit inserts into the jig&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/press-fit-soldering.ogg&#34; alt=&#34;Soldering the press-fit inserts into the jig&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/press-fit-soldering.avi&#34; alt=&#34;Soldering the press-fit inserts into the jig&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/press-fit-soldering.mov&#34; alt=&#34;Soldering the press-fit inserts into the jig&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/press-fit-soldering.mp4&#34; alt=&#34;Soldering the press-fit inserts into the jig&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Captive nut jig:&lt;/strong&gt;
The holes need to be small enough to hold the nut steady as a screw is driven into it.&lt;/p&gt;

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;400px&#34; style=&#34;max-width:400px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-captive-iterating.webm&#34; alt=&#34;Testing a captive nut jig&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-captive-iterating.ogg&#34; alt=&#34;Testing a captive nut jig&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-captive-iterating.avi&#34; alt=&#34;Testing a captive nut jig&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-captive-iterating.mov&#34; alt=&#34;Testing a captive nut jig&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/screw-captive-iterating.mp4&#34; alt=&#34;Testing a captive nut jig&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-cutting screw jig:&lt;/strong&gt;
The holes need to be small enough that the screws cut their threads in the plastic.&lt;/p&gt;

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2022/start-openscad/final-jigs.resized.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2022/start-openscad/final-jigs.resized.jpg&#34; class=&#34;post-image &#34; alt=&#34;All 3 screw jigs printed out and set up&#34; width=&#34;700px&#34; style=&#34;max-width:700px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;It took a lot of iterations, but I finally settled on values that can reliably reproduce ideal screw sizes for my workflow. I created &amp;ldquo;official&amp;rdquo; screw modules so that I can reuse them in new SCAD files.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/matt-fff/scadlib/blob/a4d342d0c854122aeaec122ed67b3b1be64ea69b/screws.scad&#34;&gt;screws.scad&lt;/a&gt;

&lt;pre&gt;&lt;code  class=&#34;language-javascript&#34;&gt;include &amp;lt;BOSL2/std.scad&amp;gt;;

module m2_hole(height, self_cut=true, anchor=CENTER) {
    diam = (self_cut) ? 2.2 : 3;
    cyl(
        d=diam,
        h=height,
        anchor=anchor
    );
}
...
module m5_hole(height, self_cut=true, anchor=CENTER) {
    diam = (self_cut) ? 5 : 6;
    cyl(
        d=diam,
        h=height,
        anchor=anchor
    );
}

module m2_captive_nut(height, inset=2, anchor=CENTER) {
    width = 4.5;
    cuboid(
        [width, width &amp;#43; inset &amp;#43; 1.5, height],
        anchor=anchor
    );
}
...
module m5_captive_nut(height, inset=3, anchor=CENTER) {
    width = 9;
    cuboid(
        [width, width &amp;#43; inset &amp;#43; 1.5, height],
        anchor=anchor
    );
}
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h1 id=&#34;reusing-the-screw-things&#34;&gt;Reusing the screw things&lt;/h1&gt;
&lt;p&gt;I now have a library of screw holes that I know will work with my screws and 3D printing process.
Now what?&lt;/p&gt;
&lt;p&gt;I have four &lt;a href=&#34;https://www.amazon.com/gp/product/B0796RR3MQ&#34;&gt;brushless motors&lt;/a&gt; that I plan on using soon, so it makes sense to start there. Each motor has four m3 screws arranged around its core.&lt;/p&gt;

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2022/start-openscad/motor-screw-holes.resized.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2022/start-openscad/motor-screw-holes.resized.jpg&#34; class=&#34;post-image &#34; alt=&#34;Brushless motor screws&#34; width=&#34;800px&#34; style=&#34;max-width:800px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;As you can see, the screw holes are somewhat asymmetric. There&amp;rsquo;s an additional 3mm between the wider set of screws.&lt;/p&gt;
&lt;p&gt;A tip for finding the distance between two screw holes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Measure the distance between the outer edges of the holes&lt;/li&gt;
&lt;li&gt;Measure the distance between the inner edges of the holes&lt;/li&gt;
&lt;li&gt;Add the distances together and divide by two.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/matt-fff/scadlib/blob/a4d342d0c854122aeaec122ed67b3b1be64ea69b/drone/motor.scad&#34;&gt;motor.scad&lt;/a&gt;

&lt;pre&gt;&lt;code  class=&#34;language-javascript&#34;&gt;include &amp;lt;BOSL2/std.scad&amp;gt;;
use &amp;lt;scadlib/screws.scad&amp;gt;;

module screw_holes(height, anchor=CENTER) {
    width1 = (13 &amp;#43; 18.5) / 2;
    width2 = (16 &amp;#43; 21.5) / 2;
    fwd(width1/2)
        m3_hole(height, self_cut=false, anchor=anchor);
    back(width1/2)
        m3_hole(height, self_cut=false, anchor=anchor);
    zrot(90)union() {
        fwd(width2/2)
            m3_hole(height, self_cut=false, anchor=anchor);
        back(width2/2)w
            m3_hole(height, self_cut=false, anchor=anchor);
    }
}

module air_hole(height) {
    cuboid(
        // idk why the height needs bumped
        [8, 4, height * 1.01],
        anchor=BOTTOM,
        edges=&amp;#34;Z&amp;#34;,
        rounding=2
    );
}

module air_holes(height) {
    width = (13 &amp;#43; 22.5) / 2;
    fwd(width/2)air_hole(height);
    back(width/2)air_hole(height);
    zrot(90)union() {
        fwd(width/2)air_hole(height);
        back(width/2)air_hole(height);
    }
}


module motor_mount(
    motor_diam=28,
    shell_thickness=1.5,
    shell_lip=5,
    wire_width=7,
){
    double_thick = shell_thickness * 2;
    difference() {
        // shell
        cyl(
            d=motor_diam &amp;#43; double_thick,
            h=shell_lip &amp;#43; shell_thickness,
            anchor=BOTTOM,
            chamfer2=1
        );
        up(shell_thickness)union() {
            // motor negative space
            cyl(
                d=motor_diam,
                h=shell_lip &amp;#43; shell_thickness,
                anchor=BOTTOM,
                chamfer=1
            );
            // wire negative space
            fwd(shell_thickness)cuboid(
            [
                wire_width,
                motor_diam,
                shell_lip &amp;#43; shell_thickness
            ], anchor=BOTTOM);

            // holes
            down(shell_thickness)union() {
                // m3 screw holes
                zrot(-45)
                    screw_holes(shell_thickness, anchor=BOTTOM);
                // air holes
                air_holes(shell_thickness);
            }
        }
    }
}

motor_mount();
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;The resulting model:&lt;/p&gt;

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;600px&#34; style=&#34;max-width:600px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/brushless-mount.webm&#34; alt=&#34;Brushless motor mount design in OpenSCAD&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/brushless-mount.ogg&#34; alt=&#34;Brushless motor mount design in OpenSCAD&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/brushless-mount.avi&#34; alt=&#34;Brushless motor mount design in OpenSCAD&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/brushless-mount.mov&#34; alt=&#34;Brushless motor mount design in OpenSCAD&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/brushless-mount.mp4&#34; alt=&#34;Brushless motor mount design in OpenSCAD&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;p&gt;I printed out the model to make sure it actually matched the motors. Perfect fit.&lt;/p&gt;

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; width=&#34;600px&#34; style=&#34;max-width:600px&#34;&gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/brushless-mount-fitted.webm&#34; alt=&#34;Brushless motor mounted with 3d print&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/brushless-mount-fitted.ogg&#34; alt=&#34;Brushless motor mounted with 3d print&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/brushless-mount-fitted.avi&#34; alt=&#34;Brushless motor mounted with 3d print&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/brushless-mount-fitted.mov&#34; alt=&#34;Brushless motor mounted with 3d print&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2022/start-openscad/brushless-mount-fitted.mp4&#34; alt=&#34;Brushless motor mounted with 3d print&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

</description>
    </item>
    
    <item>
      <title>Chardet: Easy Codec Detection in Python</title>
      <link>https://matt-w.net/blog/encoding-detection/</link>
      <pubDate>Sun, 19 Jun 2022 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/encoding-detection/</guid>
      <description>&lt;p&gt;If you have to deal with uploads from real-life users, you run into encoding issues from time to time. This is especially pernicious with CSV files, which Excel loves to export with strange and Windows-specific latin codecs.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What&amp;rsquo;s with the garbled characters? What the heck darn kind of outfit are you running here?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;^ Feedback from a client, probably&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;enter-chardet&#34;&gt;Enter chardet&lt;/h2&gt;
&lt;p&gt;chardet is a character encoding detector from the good folks at Mozilla. While &lt;a href=&#34;https://hg.mozilla.org/mozilla-central/file/bc7deafdac4b/extensions/universalchardet&#34;&gt;originally written for C++&lt;/a&gt;, we&amp;rsquo;ll be using the &lt;a href=&#34;https://chardet.readthedocs.io/en/latest/&#34;&gt;Python port&lt;/a&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;pip install chardet
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;chardet looks at the data you feed it and makes an educated guess about the codec. Here&amp;rsquo;s a very basic codec guessing function:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;from chardet.universaldetector import UniversalDetector

DEFAULT_ENCODING = &amp;#34;utf-8&amp;#34;

def guess_codec(file_name: str) -&amp;gt; str:
    codec_detector = UniversalDetector()
    with open(file_name, mode=&amp;#34;rb&amp;#34;) as file:
        while True:
            line = file.readline()
            if not line:
                break

            codec_detector.feed(line)
            if codec_detector.done:
                break

    result = codec_detector.close()
    encoding = result.get(&amp;#34;encoding&amp;#34;)
    return encoding or DEFAULT_ENCODING
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I understand that if you&amp;rsquo;re dealing with user uploads, there&amp;rsquo;s a pretty tight ceiling on your happiness, but I hope this helps in a marginal way.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>7 Deadly API Sins</title>
      <link>https://matt-w.net/blog/7-deadly-api-rules/</link>
      <pubDate>Tue, 22 Mar 2022 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/7-deadly-api-rules/</guid>
      <description>&lt;h1 id=&#34;maximum-suffering---minimal-effort&#34;&gt;Maximum suffering - minimal effort&lt;/h1&gt;
&lt;p&gt;This guide and the descriptions below are fictitious.
No identification with actual APIs (living or dead) is intended or should be inferred - especially not seller, vendor, marketplace, or advertising APIs from companies named after notable rainforests.&lt;/p&gt;
&lt;h2 id=&#34;1-keep-your-documentation-accuracy-as-close-to-666-as-possible&#34;&gt;1. Keep your documentation accuracy as close to 66.6% as possible&lt;/h2&gt;
&lt;p&gt;Obviously 66.6% looks rather insidious, but that&amp;rsquo;s purely coincidental.&lt;/p&gt;
&lt;p&gt;Found through extensive trial and error, 66.6% is the &amp;ldquo;golden ratio&amp;rdquo; for API documentation. Your docs have to be accurate enough to keep API users referencing them, but inaccurate enough to cause untold suffering.&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;most&lt;/em&gt; fun part of this is redirecting users to the docs when they inevitably open support tickets about everything else in this guide.&lt;/p&gt;
&lt;h2 id=&#34;2-use-completely-different-ids-for-your-ui-and-your-api&#34;&gt;2. Use completely different ID&amp;rsquo;s for your UI and your API&lt;/h2&gt;
&lt;p&gt;Using different ID&amp;rsquo;s is only half the battle; you need to make sure there&amp;rsquo;s literally no way for API users to map between the IDs in the UI and the IDs in the API.&lt;/p&gt;
&lt;p&gt;Make integrators try to guess which resource is which based on fields that aren&amp;rsquo;t unique in any way. That&amp;rsquo;s half the fun.&lt;/p&gt;
&lt;p&gt;Bonus points if you have a third layer of completely separate object IDs for internal use.&lt;/p&gt;
&lt;h2 id=&#34;3-rotate-http-codes-and-messages-for-the-exact-same-errors&#34;&gt;3. Rotate HTTP codes and messages for the exact same errors&lt;/h2&gt;
&lt;p&gt;API integrators get complacent if you leave their error handling intact for too long.&lt;/p&gt;
&lt;p&gt;As a rule, make your errors as vague as possible, but when you can&amp;rsquo;t avoid specificity, make it a point to reword and reclassify errors at regular (but not &lt;em&gt;too&lt;/em&gt; regular) intervals.&lt;/p&gt;
&lt;p&gt;You can rephrase &amp;ldquo;this region is not supported&amp;rdquo; in many ways - &amp;ldquo;support is not available in this region&amp;rdquo;, &amp;ldquo;support for this region is not yet implemented&amp;rdquo;, &amp;ldquo;this operation is not available in this marketplace&amp;rdquo;, etc.&lt;/p&gt;
&lt;p&gt;And don&amp;rsquo;t forget to change your &lt;a href=&#34;https://en.wikipedia.org/wiki/List_of_HTTP_status_codes&#34;&gt;response codes&lt;/a&gt;. Change some &lt;code&gt;429 Too Many Requests&lt;/code&gt; to &lt;code&gt;401 Unauthorized&lt;/code&gt;&amp;rsquo;s. Change some &lt;code&gt;401 Unauthorized&lt;/code&gt;&amp;rsquo;s to &lt;code&gt;403 Forbidden&lt;/code&gt;&amp;rsquo;s. Hell, change some &lt;code&gt;400 Bad Request&lt;/code&gt;&amp;rsquo;s to &lt;code&gt;200 OK&lt;/code&gt;&amp;rsquo;s - you get the idea.&lt;/p&gt;
&lt;p&gt;The important thing is to make sure your errors change all the time. Let loose and don&amp;rsquo;t be afraid to experiment!&lt;/p&gt;
&lt;h2 id=&#34;4-use-patch-and-put-interchangeably&#34;&gt;4. Use PATCH and PUT interchangeably&lt;/h2&gt;
&lt;p&gt;HTTP verbs are more what we&amp;rsquo;d call &amp;ldquo;guidelines&amp;rdquo;, after all. Use &lt;code&gt;PUT&lt;/code&gt; to mean &lt;code&gt;PATCH&lt;/code&gt; on 70-90% of endpoints. While tempting, don&amp;rsquo;t make the mistake of doing it too consistently - your users will figure it out.&lt;/p&gt;
&lt;p&gt;For the 30-10% of the time when you &lt;em&gt;really&lt;/em&gt; mean &lt;code&gt;PUT&lt;/code&gt;, it&amp;rsquo;s quite fun to require fields that the user has no way of retrieving - &lt;em&gt;&lt;strong&gt;never&lt;/strong&gt;&lt;/em&gt; return all the required data in your &lt;code&gt;GET&lt;/code&gt; endpoints.&lt;/p&gt;
&lt;p&gt;If API users ask about these mystery fields, pretend to miss the point and lecture them about how &lt;code&gt;PUT&lt;/code&gt; requests work. Direct them to the documentation.&lt;/p&gt;
&lt;h2 id=&#34;5-by-frugal---reuse-old-ids-for-new-objects&#34;&gt;5. By frugal - reuse old ID&amp;rsquo;s for new objects&lt;/h2&gt;
&lt;p&gt;When API objects can finally be retired, it would be wasteful to retire the unique identifier with them. With rising support for renewables across virtually all demographics, the only thing better than a unique ID is a recycled ID.&lt;/p&gt;
&lt;p&gt;Sustainable practices &lt;strong&gt;and&lt;/strong&gt; invalidating countless amounts of user data at the same time? What&amp;rsquo;s not to love?&lt;/p&gt;
&lt;p&gt;Like with #2, it&amp;rsquo;s critical to ensure that API users have no mechanism to determine which ID maps to which object. It&amp;rsquo;s pointless to invalidate swaths of data when your users can simply &amp;ldquo;undo it&amp;rdquo; by remapping millions and millions of foreign keys.&lt;/p&gt;
&lt;h2 id=&#34;6-consistency-is-the-enemy-of-curiosity&#34;&gt;6. Consistency is the enemy of curiosity&lt;/h2&gt;
&lt;p&gt;In a vacuum, different teams will naturally produce very different APIs. Inertia is on your side - the hard part is maintaining that vacuum.&lt;/p&gt;
&lt;p&gt;The sad truth is - many engineers will naturally want to provide a consistent experience for users. The best defence is to ensure that your devs have no concept of what &amp;lsquo;consistent&amp;rsquo; would even look like.&lt;/p&gt;
&lt;p&gt;Block teams off from existing work and documention. Cross-team coordination is completely out. If you find your team building consistent APIs - even accidentally, fabricate &amp;ldquo;user feedback&amp;rdquo; to guide them in a different direction.&lt;/p&gt;
&lt;h2 id=&#34;7-throttling-and-rate-limits-are-best-communicated-by-choosing-error-responses-at-random&#34;&gt;7. Throttling and rate limits are best communicated by choosing error responses at random&lt;/h2&gt;
&lt;p&gt;More and more, API statistics are showing a sharp drop in API usage immediately following HTTP &lt;code&gt;429 Too Many Requests&lt;/code&gt; responses. This kind of user disloyalty will not reflect well on the team when placed in the quarterly powerpoint presentation.&lt;/p&gt;
&lt;p&gt;How do you keep your users sending requests when you&amp;rsquo;re throttling them? Simple - you make sure they have no idea they&amp;rsquo;re being throttled.&lt;/p&gt;
&lt;p&gt;Send &lt;code&gt;429 Too Many Requests&lt;/code&gt; responses &lt;em&gt;just&lt;/em&gt; often enough to make API users think they can rely on it. Now rotate in &lt;code&gt;400 Bad Request&lt;/code&gt;, &lt;code&gt;401 Unauthorized&lt;/code&gt;, &lt;code&gt;403 Forbidden&lt;/code&gt;, &lt;code&gt;500 Internal Service Error&lt;/code&gt;, &lt;code&gt;502 Bad Gateway&lt;/code&gt;, &lt;code&gt;504 Gateway Timeout&lt;/code&gt; - all seemingly at random.&lt;/p&gt;
&lt;p&gt;As a bonus, you can place the user authentication tokens in the error response body. This way, API consumers who save unexpected errors might accidentally log the auth tokens in plaintext.&lt;/p&gt;
&lt;p&gt;Ignore any support tickets that suggest exposing credentials in response bodies may be inadvisable. Direct them to the documentation.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>A Terminal Case of Workspace Management</title>
      <link>https://matt-w.net/blog/workspace-management/</link>
      <pubDate>Sun, 30 Aug 2020 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/workspace-management/</guid>
      <description>&lt;p&gt;Who is this for?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You work in the terminal a lot&lt;/li&gt;
&lt;li&gt;You have to jump between projects&lt;/li&gt;
&lt;li&gt;You think easy context switching might make you curse less&lt;/li&gt;
&lt;li&gt;(Bonus) you use terminal-based editors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lately I&amp;rsquo;ve found myself with an D-tier superpower - near-painless workspace switching.&lt;/p&gt;

&lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; &gt;
    &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2020/workspace-management/workspace-jumping.webm&#34; alt=&#34;workspace jumping&#34;&gt;
    &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2020/workspace-management/workspace-jumping.ogg&#34; alt=&#34;workspace jumping&#34;&gt;
    &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2020/workspace-management/workspace-jumping.avi&#34; alt=&#34;workspace jumping&#34;&gt;
    &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2020/workspace-management/workspace-jumping.mov&#34; alt=&#34;workspace jumping&#34;&gt;
    &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2020/workspace-management/workspace-jumping.mp4&#34; alt=&#34;workspace jumping&#34;&gt;
    Your browser doesn&#39;t support this video.
&lt;/video&gt;&lt;/p&gt;

&lt;h1 id=&#34;normalizing-your-workflow&#34;&gt;Normalizing your workflow&lt;/h1&gt;
&lt;p&gt;I regularly work with a dozen different projects - none of which have consistent command usage. For example, to run the unit test suite for any given project:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;# These are all equivalent, depending on the project
pytest tests
poetry run pytest tests
docker-compose run whatever-project pytest tests
docker-compose run whatever-project python manage.py test
docker-compose -f ./compose/docker-compose-test.yml run whatever-project pytest tests
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is cognitive overhead - and usually involves a few screwups each time I context switch. I wanted a setup where I could simply type &lt;code&gt;test&lt;/code&gt; in each project and not have to think about it.&lt;/p&gt;
&lt;h2 id=&#34;enter-desk&#34;&gt;Enter desk&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/jamesob/desk&#34;&gt;desk&lt;/a&gt; is a lightweight workspace manager - essentially just a wrapper for shell scripts that define each workspace.&lt;/p&gt;
&lt;p&gt;I maintain a &lt;code&gt;deskfile&lt;/code&gt; (really just a &lt;code&gt;.sh&lt;/code&gt; file) for each project I work with regularly. By default, deskfiles live at &lt;code&gt;~/.desk/desks/&lt;/code&gt; - a great folder to keep under version control.&lt;/p&gt;
&lt;p&gt;So what goes into a desk workspace? Whatever fits your workflow. For me, the vast majority falls under:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;run&lt;/strong&gt; - to run the project&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;lint&lt;/strong&gt; - for any linting tools&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;test&lt;/strong&gt; - for the test runner&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;build&lt;/strong&gt; - to build the project and/or container&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;format&lt;/strong&gt; - for code formatting&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;interact&lt;/strong&gt; - to enter a project&amp;rsquo;s docker container&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;typecheck&lt;/strong&gt; - to run static type analysis&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are simply aliases in shell files; you can do whatever makes your workflow easier - as sparse or comprehensive as you want. Here&amp;rsquo;s how a couple of mine look.&lt;/p&gt;
&lt;h3 id=&#34;a-project-based-on-python-poetryhttpsgithubcompython-poetrypoetry&#34;&gt;A project based on Python &lt;a href=&#34;https://github.com/python-poetry/poetry&#34;&gt;Poetry&lt;/a&gt;&lt;/h3&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;#!/bin/bash
#
# Description: video editing library
#

basedir=~/Workspaces/matt/ergot

cd $basedir || exit

# code formatting
alias format=&amp;#34;poetry run black .&amp;#34;

# run unit tests
alias test=&amp;#34;poetry run pytest tests/&amp;#34;

# build the project
alias build=&amp;#34;poetry build&amp;#34;

# static type checking
alias typecheck=&amp;#34;poetry run mypy .&amp;#34;

# run flake8
alias flake8=&amp;#34;poetry run flake8 --max-complexity 10 .&amp;#34;

# run pylint
alias pylint=&amp;#34;poetry run pylint tests/ ergot/&amp;#34;

# run linting
alias lint=&amp;#34;flake8 &amp;amp;&amp;amp; pylint&amp;#34;

# run bandit security check
alias bandit=&amp;#34;poetry run bandit -r ergot/&amp;#34;

# run code quality
alias quality=&amp;#34;test; format; typecheck; bandit; lint&amp;#34;

# run the ergot command line
alias run=&amp;#34;poetry run python main.py&amp;#34;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;a-project-with-the-development-flow-built-in-docker-compose&#34;&gt;A project with the development flow built in docker compose&lt;/h3&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;#!/bin/bash
#
# Description: scheduled and queue driven jobs
#

basedir=~/Workspaces/matt/JobSlob

export COMPOSEPREFIX=&amp;#34;docker-compose -f ${basedir}/docker-compose.yaml&amp;#34;
export PYTHONPATH=$basedir
cd $basedir || exit

# close running instances
alias down=&amp;#34;$(echo &amp;#34;${COMPOSEPREFIX}&amp;#34;) down --remove-orphans&amp;#34;

# code formatting
alias format=&amp;#34;$(echo &amp;#34;${COMPOSEPREFIX}&amp;#34;) run jobslob formatting&amp;#34;

# run unit tests
alias test=&amp;#34;$(echo &amp;#34;${COMPOSEPREFIX}&amp;#34;) run jobslob test&amp;#34;

# build the project
alias build=&amp;#34;$(echo &amp;#34;${COMPOSEPREFIX}&amp;#34;) build&amp;#34;

# static type checking
alias typecheck=&amp;#34;$(echo &amp;#34;${COMPOSEPREFIX}&amp;#34;) run jobslob typecheck&amp;#34;

# run linting
alias lint=&amp;#34;$(echo &amp;#34;${COMPOSEPREFIX}&amp;#34;) run jobslob lint&amp;#34;

# run code quality
alias quality=&amp;#34;test; format; typecheck; lint&amp;#34;

# run bash inside the app container
alias interact=&amp;#34;$(echo &amp;#34;${COMPOSEPREFIX}&amp;#34;) run --entrypoint bash jobslob&amp;#34;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;I&amp;rsquo;d welcome a better solution than my weird &lt;code&gt;echo &amp;quot;${COMPOSEPREFIX}&amp;quot;&lt;/code&gt; nonsense. Email me, shell experts.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Obviously some projects will need more or fewer aliases, but these workspaces are much more uniform than before. It doesn&amp;rsquo;t matter which workspace I&amp;rsquo;m in - &lt;code&gt;test&lt;/code&gt; runs the test suite. Whatever you want.&lt;/p&gt;
&lt;p&gt;If you need to know what&amp;rsquo;s available, desk will give you a nice rundown of commands.&lt;/p&gt;

 &lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; &gt;
     &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2020/workspace-management/ergot-desk.webm&#34; alt=&#34;desk workspace list command&#34;&gt;
     &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2020/workspace-management/ergot-desk.ogg&#34; alt=&#34;desk workspace list command&#34;&gt;
     &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2020/workspace-management/ergot-desk.avi&#34; alt=&#34;desk workspace list command&#34;&gt;
     &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2020/workspace-management/ergot-desk.mov&#34; alt=&#34;desk workspace list command&#34;&gt;
     &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2020/workspace-management/ergot-desk.mp4&#34; alt=&#34;desk workspace list command&#34;&gt;
     Your browser doesn&#39;t support this video.
 &lt;/video&gt;&lt;/p&gt;

&lt;h1 id=&#34;persisting-and-switching-between-your-workspaces&#34;&gt;Persisting and switching between your workspaces&lt;/h1&gt;
&lt;p&gt;Desk is great for rapidly switching between terminal workspaces, but we can still do better. This is where terminal multiplexers come in.&lt;/p&gt;
&lt;p&gt;To borrow from &lt;a href=&#34;https://github.com/tmux/tmux/wiki&#34;&gt;tmux&amp;rsquo;s&lt;/a&gt; wiki (which says this far better than I could), terminal multiplexers:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[let] you switch easily between several programs in one terminal, detach them (they keep running in the background) and reattach them to a different terminal.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With tmux, we can easily keep many workspaces running simultaneously and rapidly switch between them.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll focus on integrating desk with tmux, but I&amp;rsquo;m sure the same thing can be accomplished with other multiplexers like &lt;a href=&#34;https://linuxize.com/post/how-to-use-linux-screen/&#34;&gt;screen&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;new-to-tmux&#34;&gt;New to tmux?&lt;/h3&gt;
&lt;p&gt;If you need a quickstart, tmux has a very solid &lt;a href=&#34;https://github.com/tmux/tmux/wiki/Getting-Started&#34;&gt;getting started guide&lt;/a&gt; and there are countless tutorials out there that I can&amp;rsquo;t beat.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href=&#34;https://tmuxcheatsheet.com/&#34;&gt;tmux cheat sheet&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;For our purposes, the following should be enough.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tmux uses &lt;code&gt;ctrl+b&lt;/code&gt; as its prefix hotkey by default. After entering &lt;code&gt;ctrl+b&lt;/code&gt;, you can either type a hotkey sequence or use &lt;code&gt;:&lt;/code&gt; and enter a command.&lt;/li&gt;
&lt;li&gt;to detach from a running tmux session: &lt;code&gt;ctrl+b, d&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;to list and switch between running sessions: &lt;code&gt;ctrl+b, s&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;(Optional) to create a new named session from &lt;em&gt;within&lt;/em&gt; tmux: &lt;code&gt;ctrl+b, :new -s new-session-name&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;integrating-with-desk&#34;&gt;Integrating with desk&lt;/h2&gt;
&lt;p&gt;The real magic happens when you add the following to your &lt;code&gt;.bashrc&lt;/code&gt;/&lt;code&gt;.zshrc&lt;/code&gt;/&lt;code&gt;.whatevershellrc&lt;/code&gt; file:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;tsession=$([[ -n &amp;#34;${TMUX&amp;#43;set}&amp;#34; ]] &amp;amp;&amp;amp; tmux display-message -p &amp;#34;#S&amp;#34;)

# strip suffixes so that you can make
# multiple instances of the same desk
# by using deskname-1, deskname-2, etc
desk=$(echo &amp;#34;$tsession&amp;#34; | sed -E &amp;#34;s/-[0-9]&amp;#43;$//g&amp;#34;)

if [ &amp;#34;x$tsession&amp;#34; != &amp;#34;x&amp;#34; ] &amp;amp;&amp;amp; [ &amp;#34;$tsession&amp;#34; != &amp;#34;$WITHIN_DESK&amp;#34; ]
then
	export WITHIN_DESK=&amp;#34;$tsession&amp;#34;
	desk . &amp;#34;$desk&amp;#34;
fi;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This snippet sets up your shell to detect whether you&amp;rsquo;re running in a tmux session. If you are, the shell will attempt to activate a desk with the same name as the session.&lt;/p&gt;
&lt;p&gt;At this stage, if you have a desk called &lt;code&gt;whatevs&lt;/code&gt;, you can run &lt;code&gt;tmux new-session -s whatevs&lt;/code&gt; to create a new tmux session that automatically activates your &lt;code&gt;whatevs&lt;/code&gt; desk. If it&amp;rsquo;s already running, you could use &lt;code&gt;tmux attach -t whatevs&lt;/code&gt; to attach to it.&lt;/p&gt;
&lt;p&gt;But again, we can make it easier.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;t () {
  tmux attach -t $1 || tmux new-session -s $1
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Adding this to your &lt;code&gt;.bashrc&lt;/code&gt;/&lt;code&gt;.zshrc&lt;/code&gt;/&lt;code&gt;.whatevershellrc&lt;/code&gt; file, you can just run &lt;code&gt;t whatevs&lt;/code&gt; and not worry about its state. tmux will drop you into your workspace.&lt;/p&gt;
&lt;h2 id=&#34;take-advantage-of-tmux-session-switching&#34;&gt;Take advantage of tmux session switching&lt;/h2&gt;
&lt;p&gt;This is how you switch between open workspaces quickly. By default, tmux will give you a list of sessions when you enter &lt;code&gt;ctrl+b, s&lt;/code&gt;. You can navigate the list with your arrow keys or use the numbers displayed alongside each session.&lt;/p&gt;

 &lt;p&gt;&lt;video autoplay loop muted playsinline controls class=&#34;post-webm&#34; &gt;
     &lt;source type=&#34;video/webm&#34; src=&#34;https://matt-w.net/img/posts/2020/workspace-management/session-switching.webm&#34; alt=&#34;tmux session switching&#34;&gt;
     &lt;source type=&#34;video/ogg&#34; src=&#34;https://matt-w.net/img/posts/2020/workspace-management/session-switching.ogg&#34; alt=&#34;tmux session switching&#34;&gt;
     &lt;source type=&#34;video/avi&#34; src=&#34;https://matt-w.net/img/posts/2020/workspace-management/session-switching.avi&#34; alt=&#34;tmux session switching&#34;&gt;
     &lt;source type=&#34;video/quicktime&#34; src=&#34;https://matt-w.net/img/posts/2020/workspace-management/session-switching.mov&#34; alt=&#34;tmux session switching&#34;&gt;
     &lt;source type=&#34;video/mp4&#34; src=&#34;https://matt-w.net/img/posts/2020/workspace-management/session-switching.mp4&#34; alt=&#34;tmux session switching&#34;&gt;
     Your browser doesn&#39;t support this video.
 &lt;/video&gt;&lt;/p&gt;

&lt;p&gt;It&amp;rsquo;s so easy to jump between projects - and even more useful if you rely on terminal-based editors. I&amp;rsquo;ve never had such a fluid workflow.&lt;/p&gt;
&lt;h1 id=&#34;deleted-scenes&#34;&gt;Deleted scenes&lt;/h1&gt;
&lt;h2 id=&#34;alternatives&#34;&gt;Alternatives&lt;/h2&gt;
&lt;h3 id=&#34;direnv&#34;&gt;direnv&lt;/h3&gt;
&lt;p&gt;If desk doesn&amp;rsquo;t suite your purposes (to be fair, it hasn&amp;rsquo;t had much development lately), I&amp;rsquo;ve found that &lt;a href=&#34;https://github.com/direnv/direnv&#34;&gt;direnv&lt;/a&gt; can meet many of the same needs.&lt;/p&gt;
&lt;p&gt;For me, the advantage goes to desk for the following reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;desk stores workspaces in a single folder that&amp;rsquo;s trivial to put in version control&lt;/li&gt;
&lt;li&gt;direnv would require a completely different method to integrate with tmux in a similar way&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;tmuxinator&#34;&gt;tmuxinator&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/tmuxinator/tmuxinator&#34;&gt;tmuxinator&lt;/a&gt; seems like it could be quite useful for workspace management and customization, but I&amp;rsquo;ve never personally used it.&lt;/p&gt;
&lt;h2 id=&#34;desired-future-improvements&#34;&gt;Desired future improvements&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Cleanly integrate &lt;a href=&#34;https://www.passwordstore.org/&#34;&gt;pass&lt;/a&gt; or some other credential manager with this setup&lt;/li&gt;
&lt;li&gt;Get rid of the &lt;code&gt;not found&lt;/code&gt; warning starting tmux with a name that has no matching desk&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
    <item>
      <title>Python 3.9 Features I&#39;m Excited to See</title>
      <link>https://matt-w.net/blog/new-in-python-3.9/</link>
      <pubDate>Thu, 13 Aug 2020 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/new-in-python-3.9/</guid>
      <description>&lt;p&gt;As of 2020-08-11, the first release candidate of Python 3.9 &lt;a href=&#34;https://www.python.org/downloads/release/python-390rc1/&#34;&gt;has been released&lt;/a&gt; - officially making it Christmas in August; let&amp;rsquo;s open presents.&lt;/p&gt;
&lt;h1 id=&#34;exciting&#34;&gt;Exciting&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;Vaguely ordered by ascending excitement.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;native-dict-unions&#34;&gt;Native Dict Unions&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;&lt;a href=&#34;https://www.python.org/dev/peps/pep-0584/&#34;&gt;PEP 584&lt;/a&gt;: Add union operators to dict&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;At the moment, if you want to merge a couple of dicts, the primary methods are to use either &lt;code&gt;{**first_dict, **second_dict}&lt;/code&gt; or &lt;code&gt;first_dict.update(second_dict)&lt;/code&gt; (which modifies &lt;code&gt;first_dict&lt;/code&gt; in-place):&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;a = {0: 0, 1: 0, 2: 0}
b = {1: 1, 2: 2, 3: 3}

self.assertEqual({**a, **b}, {0: 0, 1: 1, 2: 2, 3: 3})

# `b` is modified in-place
b.update(a) 

self.assertEqual(b, {0: 0, 1: 0, 2: 0, 3: 3})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The syntax introduced in Python 3.9 is a lot more straightforward, allowing you to merge dicts with a pipe, e.g. &lt;code&gt;first_dict | second_dict&lt;/code&gt; or &lt;code&gt;first_dict |= second_dict&lt;/code&gt; (which, again, modifies &lt;code&gt;first_dict&lt;/code&gt; in-place).&lt;/p&gt;
&lt;p&gt;The order of the dicts still matters, i.e. whichever dict comes last will take precedence when there are key collisions.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;a = {0: 0, 1: 0, 2: 0}
b = {1: 1, 2: 2, 3: 3}

# `b` colliding keys `1` and `2` still take precedence over `a`
self.assertEqual(a | b, {0: 0, 1: 1, 2: 2, 3: 3})

# `b` is modified in-place
b |= a

# `a` colliding keys `1` and `2` still take precedence over `b`
self.assertEqual(b, {0: 0, 1: 0, 2: 0, 3: 3})
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;native-string-presuffix-removal&#34;&gt;Native String (Pre|Suf)fix Removal&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;&lt;a href=&#34;https://www.python.org/dev/peps/pep-0616/&#34;&gt;PEP 616&lt;/a&gt;: String methods to remove prefixes and suffixes&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s hard to overstate how often I need to strip prefixes or suffixes - and doing it in Python has been pretty verbose up to now.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;# Stripping a prefix
if some_str.startswith(some_prefix):
    some_str = some_str[len(some_prefix):]

# Stripping a suffix
if some_str.endswith(some_suffix):
    some_str = some_str[:-len(some_suffix)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Yeah, you can collapse these to one liners, but it&amp;rsquo;s still ugly as hell - and God forbid you need to do both at the same time.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m thrilled that &lt;code&gt;removeprefix&lt;/code&gt; and &lt;code&gt;removesuffix&lt;/code&gt; are finally available in Python 3.9:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;# Stripping a prefix
some_str = some_str.removeprefix(some_prefix)

# Stripping a suffix
some_str = some_str.removesuffix(some_suffix)

# What the hell? Let&amp;#39;s do both!
some_str = some_str.removeprefix(some_prefix).removesuffix(some_suffix)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;standard-collection-type-annotations&#34;&gt;Standard Collection Type Annotations&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;&lt;a href=&#34;https://www.python.org/dev/peps/pep-0585/&#34;&gt;PEP 585&lt;/a&gt;: Type hinting generics in standard collections&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is a feature I&amp;rsquo;ll be using every day. If you use type annotations to a serious extent, you&amp;rsquo;ll probably be familiar with some version of the following:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;from typing import Dict, List, Set

str_dict: Dict[str, str] = {
   &amp;#34;only&amp;#34;: &amp;#34;strings&amp;#34;,
   &amp;#34;nothing&amp;#34;: &amp;#34;else&amp;#34;,
}

float_list: List[float] = [1.1, 2.2, 3.3]

int_set: Set[int] = {1, 2, 3}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To get the benefits of type checking, you either have to import from &lt;code&gt;typing&lt;/code&gt; (like above) or import &lt;code&gt;annotations&lt;/code&gt; from &lt;code&gt;__future__&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s just an import, but it can add up in a large project - especially if you find yourself copying any type-annotated code from one file to another (or, say, from StackOverflow).&lt;/p&gt;
&lt;p&gt;With 3.9, things will be a bit cleaner:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;str_dict: dict[str, str] = {
   &amp;#34;only&amp;#34;: &amp;#34;strings&amp;#34;,
   &amp;#34;nothing&amp;#34;: &amp;#34;else&amp;#34;,
}

float_list: list[float] = [1.1, 2.2, 3.3]

int_set: set[int] = {1, 2, 3}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1 id=&#34;honorable-mention&#34;&gt;Honorable Mention&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;I&amp;rsquo;m not not excited.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;type-annotation-annotations&#34;&gt;Type Annotation.. Annotations?&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;&lt;a href=&#34;https://www.python.org/dev/peps/pep-0593/&#34;&gt;PEP 593&lt;/a&gt;: Flexible function and variable annotations&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;As more Python libraries leverage type annotations, there&amp;rsquo;s value in giving a flexible way to add metadata alongside particular types. This PEP introduces the &lt;code&gt;Annotated&lt;/code&gt; construct which takes a type and an arbitrary list of arguments that serve as metadata, e.g.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;Annotated[float, ValueRange(42, 420)]
Annotated[int, IsDivisibleBy(4), IsAfraidOfSeven(False)]
Annotated[tuple, MinLen(1), MaxLen(7)]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To support these annotations, &lt;code&gt;typing.get_type_hints&lt;/code&gt; is changing to provide an &lt;code&gt;include_extras&lt;/code&gt; kwarg (defaulting to &lt;code&gt;False&lt;/code&gt; for backwards compatibility) that indicates whether to return this additional metadata.&lt;/p&gt;
&lt;p&gt;I see how this could be a pretty useful PEP for libraries, but I don&amp;rsquo;t see a use for it in my own work, yet.&lt;/p&gt;
&lt;h1 id=&#34;good-to-know&#34;&gt;Good to Know&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;Not throwing shade or anything. Just don&amp;rsquo;t see these changing my life very much.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;__pep-615httpswwwpythonorgdevpepspep-0615-support-for-the-iana-time-zone-database-in-the-standard-library__&#34;&gt;&lt;strong&gt;&lt;a href=&#34;https://www.python.org/dev/peps/pep-0615/&#34;&gt;PEP 615&lt;/a&gt;: Support for the IANA time zone database in the standard library&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;This is a PEP that I need to spend more time with to really understand. My ridiculously dumbed-down (due to me, not you) summary is that it introduces a new &lt;code&gt;zoneinfo&lt;/code&gt; API that replaces a lot of the functionality that you&amp;rsquo;d normally want to use &lt;code&gt;pytz&lt;/code&gt; for, e.g.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;from zoneinfo import ZoneInfo
from datetime import datetime

zone = ZoneInfo(&amp;#34;America/Denver&amp;#34;)
dt = datetime(2020, 12, 9, 6, tzinfo=zone)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;__pep-692httpswwwpythonorgdevpepspep-0602-annual-release-cycle-for-python__&#34;&gt;&lt;strong&gt;&lt;a href=&#34;https://www.python.org/dev/peps/pep-0602/&#34;&gt;PEP 692&lt;/a&gt;: Annual release cycle for Python&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Python releases once a year now.&lt;/p&gt;
&lt;h2 id=&#34;__pep-617httpswwwpythonorgdevpepspep-0617-new-peg-parser-for-cpython__&#34;&gt;&lt;strong&gt;&lt;a href=&#34;https://www.python.org/dev/peps/pep-0617/&#34;&gt;PEP 617&lt;/a&gt;: New PEG parser for CPython&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Probably only relevant if you work on CPython or have an interest in compilers.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Inexact Number Comparisons in Python</title>
      <link>https://matt-w.net/blog/test-decimal-similarity-in-python/</link>
      <pubDate>Mon, 22 Jun 2020 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/test-decimal-similarity-in-python/</guid>
      <description>&lt;p&gt;It becomes tricky to write correct comparison logic when your numbers may not be &lt;em&gt;exactly&lt;/em&gt; the same. Working with anything more than basic metrics and contrived data will often result in the need to go beyond a basic comparison:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;# python
a == b

# unittest
self.assertEqual(a, b)

# pytest
assert a == b
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2020/test-decimal-similarity-in-python/chuttersnap-64cF6TQ7ee4-unsplash.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2020/test-decimal-similarity-in-python/chuttersnap-64cF6TQ7ee4-unsplash.jpg&#34; class=&#34;post-image &#34; alt=&#34;Photo by chuttersnap on Unsplash&#34;  /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;h2 id=&#34;tolerant-comparisons-in-tests&#34;&gt;Tolerant Comparisons in Tests&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re focused on tests and using &lt;a href=&#34;https://docs.pytest.org/en/latest/contents.html&#34;&gt;pytest&lt;/a&gt;, you&amp;rsquo;re golden - &lt;a href=&#34;https://docs.pytest.org/en/latest/reference.html#pytest-approx&#34;&gt;pytest.approx&lt;/a&gt; should have you covered:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;import pytest
assert a == pytest.approx(b)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unfortunately, most of my active projects use &lt;a href=&#34;https://docs.python.org/3/library/unittest.html&#34;&gt;unittest&lt;/a&gt;, so it&amp;rsquo;s natural to try to turn to &lt;a href=&#34;https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertAlmostEqual&#34;&gt;assertAlmostEqual&lt;/a&gt; to compare:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;# unittest: make sure that a and b are equal up to 2 decimal places.
self.assertAlmostEqual(a, b, 2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works well enough for confined number ranges, but test cases often involve a large variety of different metrics running through the same assertions - &lt;code&gt;assertAlmostEqual&lt;/code&gt; falls pretty flat when you&amp;rsquo;re using it to compare number ranges from &lt;code&gt;0.000001&lt;/code&gt; up to &lt;code&gt;100000000000&lt;/code&gt;.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;# This evaluates as True despite the values being quite different on a relative basis
self.assertAlmostEqual(0.000001, 0.009, 2)

# This evaluates as False despite the values being virtually identical
self.assertAlmostEqual(100000000000, 100000000000.01, 2)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;mathisclose-for-relative-tolerances&#34;&gt;math.isclose for Relative Tolerances&lt;/h2&gt;
&lt;p&gt;Obviously you can tailor every assertion to the data, but if you&amp;rsquo;re looking for something closer to pytest&amp;rsquo;s &lt;code&gt;approx&lt;/code&gt; (and available without importing a testing framework), I&amp;rsquo;ve taken to using &lt;a href=&#34;https://docs.python.org/3/library/math.html#math.isclose&#34;&gt;math.isclose&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;isclose&lt;/code&gt; allows you to compare values using a relative tolerance rather than a fixed number of decimal places:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;import math

# will return True if a and b are within 0.1% of each other
math.isclose(a, b, rel_tol=0.001)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can use the function directly or drop it into a test case helper function for your unittest or Django test classes:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;import unittest
import math

class TestWhatever(unittest.TestCase):
    def assert_is_close(self, a, b, rel_tol=0.001):
        self.assertTrue(
            math.isclose(a, b, rel_tol=rel_tol),
            # custom message, as the default failure message won&amp;#39;t be informative
            f&amp;#34;&amp;#39;{a}&amp;#39; is not within {rel_tol:.2%} of &amp;#39;{b}&amp;#39;&amp;#34;
        )
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;unchecked-speculation-on-proposed-python-ideas&#34;&gt;Unchecked Speculation on Proposed Python Ideas&lt;/h2&gt;
&lt;p&gt;The day this post first went out, I saw some discussion on the &lt;a href=&#34;https://mail.python.org/mailman3/lists/python-ideas.python.org/&#34;&gt;python-ideas&lt;/a&gt; mailing list about introducing a kind of plus-or-minus operator into Python&amp;rsquo;s grammar, e.g.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;a == b &amp;#43;- 0.05
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This would evaluate to true if &lt;code&gt;b&lt;/code&gt; was within &lt;code&gt;0.05&lt;/code&gt; above or below &lt;code&gt;a&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;While I think the syntax is pretty intuitive, I&amp;rsquo;m not sure how useful it is. As discussed above, absolute value comparisons aren&amp;rsquo;t very useful for numbers that aren&amp;rsquo;t within a confined range. Maybe if there was a clean way to indicate if it&amp;rsquo;s a relative comparison, e.g.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;# for absolute comparisons
a == b &amp;#43;- 0.05

# for relative comparisons
a == b &amp;#43;- 0.01% 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But it seems like a needless complication when &lt;code&gt;math.isclose&lt;/code&gt; is simple enough to use. We don&amp;rsquo;t need another &lt;a href=&#34;https://www.python.org/dev/peps/pep-0572/&#34;&gt;walrus operator&lt;/a&gt; debacle.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Work Around Poor Access Control: Limit GitHub Access Tokens to Specific Repos</title>
      <link>https://matt-w.net/blog/limit-tokens-with-github-accounts/</link>
      <pubDate>Mon, 08 Jun 2020 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/limit-tokens-with-github-accounts/</guid>
      <description>&lt;h2 id=&#34;irrelevant-context&#34;&gt;Irrelevant Context&lt;/h2&gt;
&lt;p&gt;I recently decided to migrate my JetBrains IDE settings from their automatic &lt;a href=&#34;https://www.jetbrains.com/help/idea/sharing-your-ide-settings.html#IDE_settings_sync&#34;&gt;settings sync&lt;/a&gt; over to storing the &lt;a href=&#34;https://www.jetbrains.com/help/idea/sharing-your-ide-settings.html#settings-repository&#34;&gt;settings in a repository&lt;/a&gt; - with far-off hopes that it&amp;rsquo;ll be less annoying and inconsistent.&lt;/p&gt;
&lt;p&gt;I created blank repos to store settings for GoLand, PyCharm, WebStorm, and DataGrip.&lt;/p&gt;
&lt;p&gt;The frustrating part is that the JetBrains repo sync doesn&amp;rsquo;t seem to support SSH, so my usual auth method is out; for my IDEs to use these repos, I have to set up a &lt;a href=&#34;https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line&#34;&gt;GitHub personal access token&lt;/a&gt; for JetBrains to use - and there&amp;rsquo;s currently no built-in way to limit access tokens to specific repositories. I only want the JetBrains access token to work for the settings repositories, specifically.&lt;/p&gt;
&lt;h1 id=&#34;the-workaround-blank-github-collaborator-accounts&#34;&gt;The Workaround: Blank GitHub Collaborator Accounts&lt;/h1&gt;
&lt;p&gt;It&amp;rsquo;s pretty straightforward to &lt;a href=&#34;https://github.com/join&#34;&gt;create a GitHub account&lt;/a&gt;. Tada. And it may be wise to &lt;a href=&#34;https://help.github.com/en/github/authenticating-to-github/configuring-two-factor-authentication&#34;&gt;enable 2FA&lt;/a&gt; for good measure.&lt;/p&gt;

 &lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2020/limit-oauth-with-github-accounts/new-github-account.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2020/limit-oauth-with-github-accounts/new-github-account.png&#34; class=&#34;post-image &#34; alt=&#34;New GitHub Collaborator Account&#34;  /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;h2 id=&#34;send-the-invites-on-your-primary-account&#34;&gt;Send the invites on your primary account&lt;/h2&gt;
&lt;p&gt;Now on each of the desired repositories, I go to &lt;strong&gt;Settings &amp;gt; Manage Access &amp;gt; Invite a collaborator&lt;/strong&gt;.&lt;/p&gt;

 &lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2020/limit-oauth-with-github-accounts/invite-a-collaborator-settings.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2020/limit-oauth-with-github-accounts/invite-a-collaborator-settings.png&#34; class=&#34;post-image &#34; alt=&#34;Invite a collaborator&#34;  /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;h2 id=&#34;accept-the-invites-on-your-collaborator-account&#34;&gt;Accept the invites on your collaborator account&lt;/h2&gt;
&lt;p&gt;Actually finding these invitations in the collaborator account is &lt;a href=&#34;https://github.community/t5/How-to-use-Git-and-GitHub/Where-do-I-see-all-incoming-invitations-collaborator/td-p/4280&#34;&gt;far harder than it should be&lt;/a&gt;. Invitations are supposed to show up in GitHub notifications, but this isn&amp;rsquo;t true for me. The 2 ways that worked for me include 1) via the invitation email GitHub sends or 2) navigating to &lt;code&gt;https://github.com/&amp;lt;your_account&amp;gt;/&amp;lt;your_invite_repo&amp;gt;/invitations&lt;/code&gt; directly. Obviously you need to be logged into the collaborator account.&lt;/p&gt;

 &lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2020/limit-oauth-with-github-accounts/accept-invitation-email.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2020/limit-oauth-with-github-accounts/accept-invitation-email.png&#34; class=&#34;post-image &#34; alt=&#34;Invitation Email&#34; width=&#34;600px&#34; style=&#34;max-width:600px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;


 &lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2020/limit-oauth-with-github-accounts/accept-invitation-gh.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2020/limit-oauth-with-github-accounts/accept-invitation-gh.png&#34; class=&#34;post-image &#34; alt=&#34;Accept Invitation on GitHub&#34;  /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;h2 id=&#34;generate-a-personal-access-token-on-your-collaborator-account&#34;&gt;Generate a personal access token on your collaborator account&lt;/h2&gt;
&lt;p&gt;Heading on over to &lt;strong&gt;Settings &amp;gt; Developer Settings &amp;gt; Personal Access Tokens&lt;/strong&gt;, the new token is ripe for the generating.&lt;/p&gt;

 &lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2020/limit-oauth-with-github-accounts/generate-token.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2020/limit-oauth-with-github-accounts/generate-token.png&#34; class=&#34;post-image &#34; alt=&#34;Generate Access Token&#34;  /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;We&amp;rsquo;re still forced to use an overly broad scope (because GitHub), but at least the token only has access to the specific repositories we set up.&lt;/p&gt;

 &lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2020/limit-oauth-with-github-accounts/generate-token-scopes.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2020/limit-oauth-with-github-accounts/generate-token-scopes.png&#34; class=&#34;post-image &#34; alt=&#34;Generate Access Token&#34; width=&#34;600px&#34; style=&#34;max-width:600px&#34; /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;Copy the plaintext token which, like us, is transient and won&amp;rsquo;t be accessible later.&lt;/p&gt;

 &lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2020/limit-oauth-with-github-accounts/generate-token-final.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2020/limit-oauth-with-github-accounts/generate-token-final.png&#34; class=&#34;post-image &#34; alt=&#34;Copy Access Token&#34;  /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;And now you have your personal access token with limited repository access.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Print Tracebacks From Python Exception Instances</title>
      <link>https://matt-w.net/blog/python-exception-traceback-pdb/</link>
      <pubDate>Mon, 25 May 2020 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/python-exception-traceback-pdb/</guid>
      <description>&lt;p&gt;&lt;strong&gt;The solution:&lt;/strong&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;import traceback; traceback.print_tb(exception_instance.__traceback__)
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The rest:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In my daily work, I use &lt;a href=&#34;https://docs.python.org/3/library/pdb.html&#34;&gt;pdb&lt;/a&gt; pretty heavily for debugging. There are some limitations to the pdb shell that are constant annoyances to me - one of which applies directly to my habit of sticking breakpoints in &lt;code&gt;try/except&lt;/code&gt; blocks. For example:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;class ExistentialCrisis(Exception):
    pass

def think_about_existence():
    raise ExistentialCrisis(&amp;#34;oblivion is inevitable&amp;#34;)

try:
    think_about_existence()
except ExistentialCrisis as exc:
    import pdb; pdb.set_trace()
    # This is where we end up in pdb after running this code.
    raise
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we weren&amp;rsquo;t in pdb, we could solve this pretty easily:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;...
try:
    think_about_existence()
except ExistentialCrisis as exc:
    import traceback
    traceback.print_exc()
    raise
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And you can do the same &lt;code&gt;traceback.print_exc()&lt;/code&gt; in pdb if you use the &lt;a href=&#34;https://docs.python.org/3/library/pdb.html#pdbcommand-interact&#34;&gt;interact&lt;/a&gt; command to drop into the current scope - but I inexplicably hate doing that.&lt;/p&gt;
&lt;p&gt;How can you get the traceback from the exception object itself?&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;import traceback; traceback.print_tb(exc.__traceback__)
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>Cloning a Repository with Submodules</title>
      <link>https://matt-w.net/blog/git-clone-with-submodules/</link>
      <pubDate>Mon, 11 May 2020 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/git-clone-with-submodules/</guid>
      <description>&lt;p&gt;Every couple months, I&amp;rsquo;ll clone a repo with submodules and have to go re-learn how to clone the submodules from &lt;a href=&#34;https://git-scm.com/book/en/v2/Git-Tools-Submodules&#34;&gt;this guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For future me, who&amp;rsquo;s forgotten again - &lt;strong&gt;how to clone a submodule in a repository you&amp;rsquo;ve already pulled:&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;git submodule init
git submodule update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you have enough foresight - &lt;strong&gt;how to download submodules at the same time you clone the main repository:&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;git clone --recurse-submodules &amp;lt;your-repository&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</description>
    </item>
    
    <item>
      <title>Hijacking Default Django &#39;Through&#39; Tables</title>
      <link>https://matt-w.net/blog/hijacking-default-django-through-tables/</link>
      <pubDate>Thu, 12 Dec 2019 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/hijacking-default-django-through-tables/</guid>
      <description>&lt;p&gt;A few times in the last year, I&amp;rsquo;ve run into the need to add some metadata to a Django many-to-many relationship. By default, there&amp;rsquo;s no explicit model to add fields to, but - if you&amp;rsquo;re working on an active project - you probably have existing data in the default &amp;rsquo;through&amp;rsquo; table that you don&amp;rsquo;t want to lose.&lt;/p&gt;
&lt;p&gt;So what are you to do if you don&amp;rsquo;t want to have to create a completely new table and migrate the data over? Let&amp;rsquo;s hijack the existing one.&lt;/p&gt;
&lt;h1 id=&#34;model-setup&#34;&gt;Model Setup&lt;/h1&gt;
&lt;p&gt;The existing models:

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;from django.db import models

class ModelA(models.Model):
    b_models = models.ManyToManyField(&amp;#34;app.ModelB&amp;#34;, related_name=&amp;#34;a_models&amp;#34;, blank=True)

    class Meta:
        db_table = &amp;#34;app_model_a&amp;#34;


class ModelB(models.Model):
    class Meta:
        db_table = &amp;#34;app_model_b&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Create a model that matches the existing &amp;rsquo;through&amp;rsquo; table exactly - and make sure to specify the existing table name using &lt;code&gt;Meta.db_table&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;from django.db import models


class ModelAModelB(models.Model):

    modela = models.ForeignKey(&amp;#34;app.ModelA&amp;#34;, on_delete=models.CASCADE)
    modelb = models.ForeignKey(&amp;#34;app.ModelB&amp;#34;, on_delete=models.CASCADE)

    class Meta:
        db_table = &amp;#34;app_model_a_model_b&amp;#34;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Update the many-to-many relationship to use the new model:

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;class ModelA(models.Model):
    b_models = models.ManyToManyField(&amp;#34;app.ModelB&amp;#34;, related_name=&amp;#34;a_models&amp;#34;, through=&amp;#34;app.ModelAModelB&amp;#34;, blank=True)

    class Meta:
        db_table = &amp;#34;app_model_a&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h1 id=&#34;migrations&#34;&gt;Migrations&lt;/h1&gt;
&lt;p&gt;Now generate a new migration with &lt;code&gt;python manage.py makemigrations&lt;/code&gt;; it&amp;rsquo;ll need some editing. The initial migration:

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;import datetime
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        (&amp;#39;api_app&amp;#39;, &amp;#39;69420_whatever_your_last_migration_was&amp;#39;),
    ]

    operations = [
        migrations.CreateModel(
            name=&amp;#39;ModelAModelB&amp;#39;,
            fields=[
                (&amp;#39;id&amp;#39;, models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=&amp;#39;ID&amp;#39;)),
            ],
            options={
                &amp;#39;db_table&amp;#39;: &amp;#39;app_model_a_model_b&amp;#39;,
            },
        ),
        migrations.AlterField(
            model_name=&amp;#39;modela&amp;#39;,
            name=&amp;#39;b_models&amp;#39;,
            field=models.ManyToManyField(blank=True, related_name=&amp;#39;a_models&amp;#39;, through=&amp;#39;app.ModelAModelB&amp;#39;, to=&amp;#39;app.ModelB&amp;#39;),
        ),
        migrations.AddField(
            model_name=&amp;#39;modelamodelb&amp;#39;,
            name=&amp;#39;modela&amp;#39;,
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=&amp;#39;app.ModelA&amp;#39;),
        ),
        migrations.AddField(
            model_name=&amp;#39;modelamodelb&amp;#39;,
            name=&amp;#39;modelb&amp;#39;,
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=&amp;#39;app.ModelB&amp;#39;),
        ),
    ]
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;h2 id=&#34;wrap-the-migration&#34;&gt;Wrap the Migration&lt;/h2&gt;
&lt;p&gt;We just need to wrap these operations in &lt;code&gt;migrations.SeparateDatabaseAndState&lt;/code&gt; to get the models in sync without screwing up the existing database setup. All of the changes above represent state changes:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;import datetime
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        (&amp;#39;api_app&amp;#39;, &amp;#39;69420_whatever_your_last_migration_was&amp;#39;),
    ]

    state_operations = [
        migrations.CreateModel(
            name=&amp;#39;ModelAModelB&amp;#39;,
            fields=[
                (&amp;#39;id&amp;#39;, models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=&amp;#39;ID&amp;#39;)),
            ],
            options={
                &amp;#39;db_table&amp;#39;: &amp;#39;app_model_a_model_b&amp;#39;,
            },
        ),
        migrations.AlterField(
            model_name=&amp;#39;modela&amp;#39;,
            name=&amp;#39;b_models&amp;#39;,
            field=models.ManyToManyField(blank=True, related_name=&amp;#39;a_models&amp;#39;, through=&amp;#39;app.ModelAModelB&amp;#39;, to=&amp;#39;app.ModelB&amp;#39;),
        ),
        migrations.AddField(
            model_name=&amp;#39;modelamodelb&amp;#39;,
            name=&amp;#39;modela&amp;#39;,
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=&amp;#39;app.ModelA&amp;#39;),
        ),
        migrations.AddField(
            model_name=&amp;#39;modelamodelb&amp;#39;,
            name=&amp;#39;modelb&amp;#39;,
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=&amp;#39;app.ModelB&amp;#39;),
        ),
    ]

    operations = [
        migrations.SeparateDatabaseAndState(
            state_operations=state_operations,
        )
    ]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And that should do it. That migration should successfully tie the existing &amp;rsquo;through&amp;rsquo; table to your new model. Now you can add fields and create additional migrations as normal.&lt;/p&gt;
&lt;h2 id=&#34;for-my-notes---filtering-on-through-fields&#34;&gt;For my notes - filtering on &amp;rsquo;through&amp;rsquo; fields&lt;/h2&gt;
&lt;p&gt;I want to make one additional note - since it wasn&amp;rsquo;t immediately obvious to me.  With Django 2.2, you should be able to filter on the &amp;rsquo;through&amp;rsquo; table fields by using the lowercase name of the &amp;rsquo;through&amp;rsquo; model.&lt;/p&gt;
&lt;p&gt;You can add something like &lt;code&gt;state&lt;/code&gt; to the &amp;rsquo;through&amp;rsquo; model:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;class ModelAModelB(models.Model):

    modela = models.ForeignKey(&amp;#34;app.ModelA&amp;#34;, on_delete=models.CASCADE)
    modelb = models.ForeignKey(&amp;#34;app.ModelB&amp;#34;, on_delete=models.CASCADE)
    state = models.CharField(
      max_length=16,
      null=True,
      blank=True,
      choices=[(&amp;#34;good&amp;#34;, &amp;#34;good&amp;#34;), (&amp;#34;ungood&amp;#34;, &amp;#34;ungood&amp;#34;)]
    )

    class Meta:
        db_table = &amp;#34;app_model_a_model_b&amp;#34;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then filtering on this field looks a lot like this:

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;# querysets
ModelA.objects.filter(modelamodelb__state=&amp;#34;good&amp;#34;)
ModelB.objects.filter(modelamodelb__state=&amp;#34;ungood&amp;#34;)
ModelAModelB.objects.filter(state=&amp;#34;good&amp;#34;)

# related manager on instances
a_instance = ModelA.objects.first()
a_instance.b_models.filter(modelamodelb__state=&amp;#34;ungood&amp;#34;)

b_instance = ModelB.objects.first()
b_instance.a_models.filter(modelamodelb__state=&amp;#34;good&amp;#34;)
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;Now hopefully I can just come back to this post next time I need to do this.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>Git Hooks: Autoformat before commit</title>
      <link>https://matt-w.net/blog/git-hooks-autoformat-before-commit/</link>
      <pubDate>Sat, 05 Jan 2019 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/git-hooks-autoformat-before-commit/</guid>
      <description>&lt;p&gt;Autoformatters can be great, keeping diffs small and a code base readable across teams and engineers. &lt;a href=&#34;https://github.com/ambv/black&#34;&gt;Black&lt;/a&gt; formatting is a standard in my Python repositories.&lt;/p&gt;

 &lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://matt-w.net/img/posts/2019/unsplash-letters.webp&#34; /&gt;&lt;img src=&#34;https://matt-w.net/img/posts/2019/unsplash-letters.jpg&#34; class=&#34;post-image &#34; alt=&#34;The typical code base&#34;  /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;But what isn&amp;rsquo;t great is forgetting to run the formatter, having the CI build fail, and having to add a &amp;ldquo;Formatting&amp;rdquo; commit. That&amp;rsquo;s where &lt;a href=&#34;https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks&#34;&gt;Git Hooks&lt;/a&gt; come in.&lt;/p&gt;
&lt;p&gt;Git hooks are stored in &lt;code&gt;.git/hooks&lt;/code&gt; folder of any given Git project. There should be some samples pre-populated. For our purposes, &lt;code&gt;pre-commit&lt;/code&gt; is the hook of interest.&lt;/p&gt;
&lt;p&gt;Lets focus on the Black formatter for Python as an example. For the example, it needs to be installed locally (&lt;code&gt;pip install black&lt;/code&gt;). Naively, we can add formatting on pre-commit by adding a file called &lt;code&gt;pre-commit&lt;/code&gt; to &lt;code&gt;.git/hooks&lt;/code&gt; with these contents:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;black .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(For Javascript, you could use &lt;a href=&#34;https://prettier.io/&#34;&gt;Prettier&lt;/a&gt; and &lt;code&gt;prettier --write **/*.js&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;Don&amp;rsquo;t forget to make the file executable (use &lt;code&gt;chmod +x .git/hooks/pre-commit&lt;/code&gt;), or the hook may not fire.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s nice and simple, but there are a couple issues with this approach. First off, Black will be running over all the source files rather than just the ones that changed (which can be quite slow, depending on the size of your project). Secondly, the modified source files won&amp;rsquo;t actually be added to the commit, which means you need to re-commit the reformatted files.&lt;/p&gt;
&lt;p&gt;Here comes the convoluted piping. To get the names and statuses of files that are staged for commit, we can use &lt;code&gt;git diff --cached --name-status&lt;/code&gt; to yield output like this:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;&amp;gt; $ git diff --cached --name-status

M       someproject/src/whatevs.py
A       someproject/tests/whatevs_test.py
D       someproject/tests/lasers.py
M       someproject/README.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We want to ignore deleted files, since we can&amp;rsquo;t format them, so let&amp;rsquo;s filter them out:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;&amp;gt; $ git diff --cached --name-status | \
grep -v &amp;#39;^D&amp;#39;

M       someproject/src/whatevs.py
A       someproject/tests/whatevs_test.py
M       someproject/README.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We want to ignore non-Python files (or Javascript or Golang or whatever you&amp;rsquo;re using), so let&amp;rsquo;s filter on file extension:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;&amp;gt; $ git diff --cached --name-status | \
grep -v &amp;#39;^D&amp;#39; | \
grep &amp;#39;.py&amp;#39;

M       someproject/src/whatevs.py
A       someproject/tests/whatevs_test.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we have the files we want, we just want to focus on the names:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;&amp;gt; $ git diff --cached --name-status | \
grep -v &amp;#39;^D&amp;#39; | \
grep &amp;#39;.py&amp;#39; | \
sed &amp;#39;s/[A-Z][ \t]*//&amp;#39;

someproject/src/whatevs.py
someproject/tests/whatevs_test.py
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;And we can pass that right into Black (or whatever formatter you&amp;rsquo;re using):&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;&amp;gt; $ git diff --cached --name-status | \
grep -v &amp;#39;^D&amp;#39; | \
grep &amp;#39;.py&amp;#39; | \
sed &amp;#39;s/[A-Z][ \t]*//&amp;#39; | \
xargs black

reformatted someproject/src/whatevs.py
reformatted someproject/tests/whatevs_test.py
All done! ✨ 🍰 ✨
2 files reformatted.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So that&amp;rsquo;s all great, but the changes still aren&amp;rsquo;t staged. Let&amp;rsquo;s fix that by processing the output from Black into just the files that were reformatted:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;&amp;gt; $ git diff --cached --name-status | \
grep -v &amp;#39;^D&amp;#39; | \
grep &amp;#39;.py&amp;#39; | \
sed &amp;#39;s/[A-Z][ \t]*//&amp;#39; | \
xargs black 2&amp;gt;&amp;amp;1 | \
grep &amp;#39;^reformatted&amp;#39; | \
sed &amp;#39;s/reformatted[ \t]//&amp;#39;

someproject/src/whatevs.py
someproject/tests/whatevs_test.py
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;And now we can finally stage the formatted files with &lt;code&gt;xargs git add&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So the final &lt;code&gt;.git/hooks/pre-commit&lt;/code&gt; file should look something like this:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;#!/bin/bash

git diff --cached --name-status | \
grep -v &amp;#39;^D&amp;#39; | grep &amp;#39;.py&amp;#39; | \
sed &amp;#39;s/[A-Z][ \t]*//&amp;#39; | \
xargs black 2&amp;gt;&amp;amp;1 | \
grep &amp;#39;^reformatted&amp;#39; | \
sed &amp;#39;s/reformatted[ \t]//&amp;#39; | \
xargs git add
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then you need to make sure it&amp;rsquo;s runnable:

&lt;pre&gt;&lt;code  class=&#34;language-bash&#34;&gt;&amp;gt; $ chmod &amp;#43;x .git/hooks/pre-commit
&lt;/code&gt;&lt;/pre&gt;&lt;/p&gt;
&lt;p&gt;That should do it.&lt;/p&gt;
</description>
    </item>
    
    <item>
      <title>SQLAlchemy &#43; FactoryBoy: Passing arbitrary sessions to factories</title>
      <link>https://matt-w.net/blog/sqlalchemy-session-wrapping-factories/</link>
      <pubDate>Mon, 02 Jul 2018 00:00:00 +0000</pubDate>
      
      <guid>https://matt-w.net/blog/sqlalchemy-session-wrapping-factories/</guid>
      <description>&lt;p&gt;In work projects in the past, my team would try to avoid dealing with the complexity of SQLAlchemy database sessions by making one global session that every module referenced. It made things easy and straightforward and it played nice with FactoryBoy - whose factories seem to work quite well under those conditions.&lt;/p&gt;

 &lt;p&gt;&lt;picture&gt;&lt;source srcset=&#34;https://images.unsplash.com/photo-1516937941344-00b4e0337589?ixlib=rb-0.3.5&amp;amp;q=80&amp;amp;fm=jpg&amp;amp;crop=entropy&amp;amp;cs=tinysrgb&amp;amp;w=1080&amp;amp;fit=max&amp;amp;ixid=eyJhcHBfaWQiOjExNzczfQ&amp;amp;s=bf504a1d5183840e87511d01ce729be2&#34; /&gt;&lt;img src=&#34;https://images.unsplash.com/photo-1516937941344-00b4e0337589?ixlib=rb-0.3.5&amp;amp;q=80&amp;amp;fm=jpg&amp;amp;crop=entropy&amp;amp;cs=tinysrgb&amp;amp;w=1080&amp;amp;fit=max&amp;amp;ixid=eyJhcHBfaWQiOjExNzczfQ&amp;amp;s=bf504a1d5183840e87511d01ce729be2&#34; class=&#34;post-image &#34; alt=&#34;Don&amp;#39;t Look At Me - Patrick Hendry&#34;  /&gt;&lt;/picture&gt;&lt;/p&gt;

&lt;p&gt;Going into building &lt;a href=&#34;http://www.lobit.io/&#34;&gt;lobit.io&lt;/a&gt;, I tried to do things the same way, but as the code base and the number of unit tests grew, I kept facing what seemed to be an unquashable avalanche of OperationalErrors griping about &amp;ldquo;Too many connections&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;I implemented an overhaul of the codebase, wrapping every piece of ORM code in a separate session and creating a DbUtils class to handle sessions consistently without too much boilerplate:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;from contextlib import contextmanager
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm.session import Session
from config import config


class DbUtils:
    Session = sessionmaker(bind=create_engine(config.db_connection_url))

    @classmethod
    @contextmanager
    def session_scope(cls) -&amp;gt; Session:
        &amp;#34;&amp;#34;&amp;#34;Provide a transactional scope around a series of operations.&amp;#34;&amp;#34;&amp;#34;
        session = cls.Session()
        try:
            yield session
            session.commit()
        except:
            session.rollback()
            raise
        finally:
            session.close()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;fixing-the-test-factories&#34;&gt;Fixing the test factories&lt;/h2&gt;
&lt;p&gt;When it came to unit tests, I was at a loss for how to best handle all of the factories I was using. They were all tied into the global session. Furthermore, factories took in the session at the time of class creation, not instantiation, e.g.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;import factory
from models.post import Post
from tests.factories.post_thread_factory import PostThreadFactory


class PostFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        model = Post
        sqlalchemy_session_persistence = &amp;#34;commit&amp;#34;
        sqlalchemy_session = session

    text = factory.Faker(&amp;#34;text&amp;#34;, max_nb_chars=100)
    external_id = factory.Faker(&amp;#34;text&amp;#34;, max_nb_chars=50)
    post_thread = factory.SubFactory(PostThreadFactory)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Wrapping factory class creation in a function did the trick:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;import factory
from models.post import Post
from tests.factories.post_thread_factory import PostThreadFactory


def PostFactory(session):
    class _PostFactory(factory.alchemy.SQLAlchemyModelFactory):
        class Meta:
            model = Post
            sqlalchemy_session_persistence = &amp;#34;commit&amp;#34;
            sqlalchemy_session = session

        text = factory.Faker(&amp;#34;text&amp;#34;, max_nb_chars=100)
        external_id = factory.Faker(&amp;#34;text&amp;#34;, max_nb_chars=50)
        post_thread = factory.SubFactory(PostThreadFactory(session))

    return _PostFactory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While simple wrapping worked, it made my tests look God-awful. Manually managing sessions had already added boilerplate overhead to every unit test that hit the database.&lt;/p&gt;
&lt;p&gt;Before refactoring, I could create a post using &lt;code&gt;PostFactory.create()&lt;/code&gt;. Now it was &lt;code&gt;PostFactory(session).create()&lt;/code&gt;. Most of the unit tests hit multiple factories several times each, so it was also less efficient to keep creating the same classes over and over.&lt;/p&gt;
&lt;p&gt;I took to creating a single instance of each required factory at the top of each session:&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;    def test_post_stuff(self):
        with DbUtils.session_scope() as session:
            post_factory = PostFactory(session)
            post_thread_factory = PostThreadFactory(session)
            ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&#34;cleaning-it-up&#34;&gt;Cleaning it up&lt;/h2&gt;
&lt;p&gt;A given test could have nearly a dozen factories defined at the top - which looked terrible even if it didn&amp;rsquo;t trigger a &lt;code&gt;too-many-locals&lt;/code&gt; warning from Pylint (which it almost always did).&lt;/p&gt;
&lt;p&gt;So I made one more change and hired a manager to keep all the factories running smoothly.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;from tests.factories.post_factory import PostFactory
from tests.factories.post_thread_factory import PostThreadFactory
...

class FactoryManager:
    known_factories = [
        PostFactory,
        PostThreadFactory,
        ...
    ]

    def __init__(self, session):
        self.session = session

        for factory_func in self.known_factories:
            setattr(self, factory_func.__name__, factory_func(self.session))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The FactoryManager didn&amp;rsquo;t require a lot of code, but let me leave my tests largely unchanged. Instead of initializing several objects at the top of each test, I could initialize one.&lt;/p&gt;

&lt;pre&gt;&lt;code  class=&#34;language-python&#34;&gt;    def test_post_stuff(self):
        with DbUtils.session_scope() as session:
            fm = FactoryManager(session)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now I could prefix all factory calls with &lt;code&gt;fm.&lt;/code&gt; and get something &lt;em&gt;almost&lt;/em&gt; as clean as before. At least, &lt;code&gt;fm.PostFactory.create()&lt;/code&gt; looks better to me than &lt;code&gt;PostFactory(session).create()&lt;/code&gt; or littering each test with a bunch of locals.&lt;/p&gt;
</description>
    </item>
    
    
  </channel>
</rss>