I have four main goals in mind for this year:
While “OctoRows” is extremely unlikely to be my project management tool’s final name, I’m very much intent on launching it in some shape. It’s been the main thing I’ve worked on lately whenever I’ve had the bandwidth, and will hopefully soon be usable in its simplest form.
Exactly what “launching” it means is still up in the air, however. I’m not sure I want the burden possibly resulting from open sourcing it, and I’m not sure I want the responsibility of having other users, either.
What’s most likely is that I’ll build it, start using it, and write about it. Then, if there’s some genuine interest, I may make it available to others in some fashion.
The email checker I’ve started and restarted a number of times in the past 15 or so months is still something I want to launch. I had grand ideas of charging a bit of money for it, but that may or may not end up being the case. This I do want to make publicly available.
I have a slowly growing collection of software I (attempt to) maintain. Most of the projects are Python based, but they don’t really share any standardized tooling around them.
Some use Poetry and some Pipenv. Some use SemVer and others CalVer. Some have tests, types, and docstrings, but probably not all. The README
s vary in structure and content. It’s a mess.
Inspired by somewhat related writings by Simon Willison and Hynek Schlawack, I’d like to bring some more sensibility to all this.
PyBeach 2020 was a resounding success, and it would be awesome to have a second event. It’s hard for me to truly promise it’s going to happen, as I worry I won’t have the bandwith required, but I’ll give an honest try.
I’ve always felt like I can’t really do group rides because my fitness isn’t great so I climb slowly and not very high, and I’d like to change that this year. So far I’ve started the year strong, and hopefully that will continue.
Happy 2024!~
Thanks for reading! You can keep up with my writing via the feed or newsletter, or you can get in touch via email or Mastodon.
]]>I just recently started taking over my wife’s home office, and had prior to that been working from the living room. My desk was near the kitchen, so sometimes she would go in to get a glass of water and want to say hi. Rather than having her constantly ask “are you busy?”, I thought I’d place a little status light on my desk that would communicate the answer preemptively.
┌─────────────────────────────────────────────────────┐
│ ┌────────────────────┐* │
│ │ │ │
│ │ DESK │ ── ── ── ── ┬─ ── ── KITCHEN │
│ │ │ │ │
│ └────────────────────┘ │
│ │ ┌─────────┘
│ │ │
│ └─────────┐
│ │ │
│ │ ENTRYWAY │
│ │
│ LIVING │ │
│ ROOM └─ ── ─┐ │
│ │
│ │ │
└──────────────────────────────────────┐ │ ┌────┘
│ │
│ HALLWAY │
│ │
│ │
Since changing the light manually required more discipline than I had, I found myself automating this rather quickly, and it turned out to be rather simple.
This is the core of our stack:
We need some additional hardware:
And some software:
We need four files:
The file should live on the device, so let’s make it /Volumes/CIRCUITPY/state
. Its initial contens are rather simple:
0
Yep, just a good old 0
.
This file should also live on the device, so let’s make it /Volumes/CIRCUITPY/code.py
, as that file will get automatically run at startup.
From the bundled library we need to import the cpx
module, which allows us to interact with the device’s inputs and outputs:
from adafruit_circuitplayground.express import cpx
Next up we want to define some colors, and we can go with green for no call and red for active call:
COLORS = (
(0, 255, 0), # green
(255, 0, 0), # red
)
We definitely want to set LED brightness to a lot less than 100% initially, as they’re very bright:
cpx.pixels.brightness = 0.01
As the device runs the code.py
file only once, we want to encapsulate the functionality inside an infinite loop. Inside that loop we want to open the status file, get the 0
or 1
we expect to find, use that value to pick the correct LED color, and finally set the LEDs to said color using the cpx
module.
while True:
with open("state") as state_file:
state = int(state_file.readline())
color = COLORS[state]
cpx.pixels.fill(color)
The file in its entirety looks like this:
from adafruit_circuitplayground.express import cpx
COLORS = (
(0, 255, 0), # green
(255, 0, 0), # red
)
cpx.pixels.brightness = 0.01
while True:
with open("state") as state_file:
state = int(state_file.readline())
color = COLORS[state]
cpx.pixels.fill(color)
The call detection script should live on your own machine. I made mine /Users/nik/bin/detect.py
, but you can keep it anywhere reasonable.
For this script we also need only one import, this time subprocess
from the standard library:
import subprocess
We want a function to encapsulate our business logic, which starts with running lsof -i 4UDP
, getting its output, converting it from a bytestring to a string, and splitting it by newline to get a list of rows. We can then look through those rows and keep only the ones which contain the word “zoom”.
def detect():
lsof_output = subprocess.check_output(["lsof", "-i", "4UDP"]).decode().split("\n")
zoom_rows = [row for row in lsof_output if "zoom" in row]
These “zoom” rows tell us whether there’s an active call or not. If Zoom isn’t running, there will be none. If Zoom is running, there may be one. And if there’s an active call, there will be more than one. This gives us the current state of things. At the same time we can initialize the state on the device before we actually read it.
current_state = int(len(zoom_rows) > 1) # 1 zoom process isn't a meeting
device_state = None
Immediately after we want to read the device state from the status file:
with open("/Volumes/CIRCUITPY/state", "r") as state_file:
device_state = int(state_file.read())
If the two states differ, we want to write the current state to the status file:
if device_state != current_state:
with open("/Volumes/CIRCUITPY/state", "w") as state_file:
state_file.write(str(current_state))
And finally, we need to make the script runnable:
if __name__ == "__main__":
detect()
The complete file looks like this:
import subprocess
def detect():
lsof_output = subprocess.check_output(["lsof", "-i", "4UDP"]).decode().split("\n")
zoom_rows = [row for row in lsof_output if "zoom" in row]
current_state = int(len(zoom_rows) > 1) # 1 zoom process isn't a meeting
device_state = None
with open("/Volumes/CIRCUITPY/state", "r") as state_file:
device_state = int(state_file.read())
if device_state != current_state:
with open("/Volumes/CIRCUITPY/state", "w") as state_file:
state_file.write(str(current_state))
if __name__ == "__main__":
detect()
The launchd service definition needs to live in your ~/Library/LaunchAgents
directory. Everything I found in mine seemed to be named something like com.{vendor}.{application}.{component}.plist
, so I made mine /Users/nik/Library/LaunchAgents/com.nik.OnAir.Detector.plist
.
We start with some standard boilerplate:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
Things get interesting when we start defining our actual service, starting with its name, which matches the file name:
<dict>
<key>Label</key>
<string>com.nik.OnAir.Detector.plist</string>
We then define the command to be run as an array of strings. Here we have a suitable Python interpreter and the path of the detection script:
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/python3</string>
<string>/Users/nik/bin/detect.py</string>
</array>
The last part is quite important, as it ensures the detection script is run continuously:
<key>KeepAlive</key>
<true/>
And the closing tags:
</dict>
</plist>
The complete file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.nik.OnAir.Detector.plist</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/python3</string>
<string>/Users/nik/bin/detect.py</string>
</array>
<key>KeepAlive</key>
<true/>
</dict>
</plist>
Now that we have all the files in place, we can run the launchd service to start the whole thing. This is done via the launchctl
command, and it looks like this:
launchctl enable com.nik.OnAir.Detector.plist
If everything is going well, you should be able to replicate this:
Hooray!~ 🎉
You might be wondering if detecting camera and/or microphone use is a better option than looking for Zoom itself. I don’t know about you, but I definitely find myself on calls with both of them disabled, at least for periods of time, so that wouldn’t work for me.
Something I didn’t cover in the talk but is super useful is a manual switch. I used xbar and a shell script to toggle the state manually.
Another thing to consider as an improvement is adding other statuses. For example, maybe yellow means “not on a call, but trying to focus”.
The future of the project for me is unclear, as it’s currently decomissioned, what with the aforementioned move into a dedicated home office which has the ultimate do-not-disturb implementation: a door. I thought about hanging it on the door knob to communicate whether knocking is OK, but then I’d have to power it, which means batteries, and I have enough charge anxiety as it is.
While ostensibly about a simple, fun, useful project, this talk is actually meant to inspire.
You see, I had no meaningful hardware experience before doing this. Sure, I’ve had some Raspberry Pis lying around and doing some things, but nothing ever really involved any hardware. It was a pretty simple thing to do, and fun!
And since I could do it, so can you! Really, if any of this makes sense, you’re not far from your own exploration.
To aid you in said exploration, I recommend gizmos like the one shown here, with easily accessible inputs and outputs. The Circuit Playground Express has a bunch of sensors —e.g., light, temperature, accelerometer—and outputs—e.g., LEDs, beeper—which allow building silly toys. After I got it at PyCon, I felt a bit inspired and built a really crappy theremin in my hotel room: I piped the light sensor values into the beeper, so I could shine a light onto the device and move my hand around to get different sounds. Then I switched to the temperature sensor and used a hair dryer. It was a good laugh.
And if this sounds like child’s play, that’s exactly my point: play is important. Most of us work with things involving phrases like “web scale” and “business value”, but we maybe got into this in the first place through some sort of play when we were younger, and it’s healthy to hold on to some of that.
So play more!
Thanks for reading! You can keep up with my writing via the feed or newsletter, or you can get in touch via email or Mastodon.
]]>On the flip side, life has only gotten busier, and I tend to have less time on the whole to devote to programming outside of work. This, in turn, has made it increasingly more difficult to make meaningful progress on anything substantial, as I find myself filling the occasional two-hour block with low hanging fruit. It’s also been getting exponentially worse lately as I somehow keep coming up with new project ideas to support the ones I’m already working on.
A big part of my problem is lack of good understanding of what I’m actually trying to work on—both which projects and which tasks within them. I’ve tried a handful of issue trackers, but nothing’s really been all that great of a fit, especially as I really like the idea of keeping GitHub issues the source of truth, at least for the open source stuff.
So I’m building my own, obviously.
I’ve currently dubbed it OctoRows, and it’s quite similar to GitHub Projects and Codetree, but without some of the deal breaking limitations.
The idea behind it isn’t particularly groundbreaking:
I’ve started prototyping the thing using Django, HTMX, and Tailwind CSS, and it’s been fun so far. Django has always been a joy to work with, but the other two tools are new to me. HTMX honestly kicks butt for someone who doesn’t really want to write a ton of frontend JavaScript, and Tailwind is making it a lot easier to build a UI prototype than futzing around with CSS would be for me.
Here’s a GIF of a very hardcoded demo:
I don’t fully know where this is headed, but I like it so far, and will continue to work on it. If it makes sense, I might launch it for public use, and/or open source it eventually, but for now it’s my own playground and emerging tool.
What do you think? Is this something you’d consider using? If so, would you consider paying for it, and how much? Any features that would be deal makers for you? I’d love some feedback, so please shoot me an email!
Thanks for reading! You can keep up with my writing via the feed or newsletter, or you can get in touch via email or Mastodon.
]]>cmd = 'pytest -m "not live"'
If you want to split it into pieces, you’d use str.split
, like so:
>>> cmd.split()
['pytest', '-m', '"not', 'live"']
But that doesn’t quite get it right, with "not live"
split into "not
and live"
. Sure, it’s technically correct, which we all know is the best kind of correct, but we probably want those two parts to go together.
Enter shlex.split
:
>>> import shlex
>>> shlex.split(cmd)
['pytest', '-m', 'not live']
shlex
is a wicked cool built-in module for lexical analysis of Unix shells.
Hat tip to Brian Okken and his podcast episode and blog post about argparse testing.
Thanks for reading! You can keep up with my writing via the feed or newsletter, or you can get in touch via email or Mastodon.
]]>tmux is a terminal multiplexer, and it offers sessions (effectively instances), windows (kind of like tabs), and panes. Within a given session you “activate” it with the ctrl-b
prefix by default, something many—myself included—remap to ctrl-a
to appease old habits from GNU Screen days. That means that, for example, creating a new window is achieved by pressing ctrl-a
followed by c
. I used this prefix probably tens or hundreds of times per day for many years.
One thing you sometimes want to do is rename a window, to distinguish it from others. By default tmux uses the active command for the window name, so it often ends up being your shell or editor. You can see them on the status line at the bottom of the window here:
I prefer to name each something more specific when I have a multi-window session, which is almost always the case:
The tmux command to do this is rename-window
, which means that you press the prefix and then type :rename-window new name goes here
, which is a tiny bit clumsy for a frequently performed operation. When I first did that this morning, I remembered I used to have a shortcut—prefix then single key—but I couldn’t find a mapping in my old config. I decided to look into it more in a little bit, then maximized the terminal window and started opening tmux windows and panes to match what I used to set up for writing on my site back in the day. Reflexively, my fingers on their own pressed the prefix and comma and I was renaming the window.
I wish I could’ve seen my own face as I realized what had just happened. 🤯
Thanks for reading! You can keep up with my writing via the feed or newsletter, or you can get in touch via email or Mastodon.
]]>TL;DR:
I think this is great, and as a result feel like I can attend in person again. The 2020 and 2021 PyCon US editions were virtual and I didn’t really get into them, followed by 2022 and 2023 in Salt Lake City, UT, when traveling and conferences didn’t quite feel safe enough. I last attended PyCon US all the way back in 2019, and recently enjoyed the excellently safe North Bay Python 2023 and PyBay 2023, so I’m excited about these safety measures. But, not everyone seems to be, including some rather prominent voices in the Python community: Python Bytes episodes 359 (4:07–9:40) and 360 (32:30–34:12). Disappointing, but perhaps predictable.
This is a simple fact, and isn’t affected by the also true fact that we wish it were. Two things can be true at once.
If you think “oh, it’s just like the flu now”, you’re quite wrong:
I dedicate this post to everyone who keeps saying, “COVID is just the flu now.”
In the most recent complete report weeks:
#COVID19 hospitalizations: 15,756
#Flu hospitalizations: 1,607COVID deaths: 1,262
Flu deaths: 14Most COVID deaths in a week THIS YEAR: 3,866
Most flu deaths in a week SINCE 2019: 1,048
Yep, exactly the same!
covid.cdc.gov/covid-data-track…
You don’t have to like it. I certainly don’t.
The Python community prides itself on being inclusive. In my years of increasing involvement I’ve seen it continually evolve to welcome more and more people into its world.
One of the PyCons I attended was the first event where I displayed my pronouns (he/him) on my badge and saw others doing the same. North Bay Python has badge lanyards communicating whether the wearer is comfortable having their photo taken. The Python Software Foundation leadership, staff, and board strive for both visible and invisible diversity. These are just some of the ways I’ve observed this priority manifest itself, and that’s from the perspective of a cishet white male who’s never been underrepresented.
I don’t recall encountering any significant pushback to these efforts. I’m sure a few folks have grumbled here and there, and people belonging to underrepresented communities have felt said pushback more strongly, but by and large I perceive this community to be forward thinking and open to doing things better over time. It’s a big reason I’m proud to be part of it.
It’s hard for me to formulate a coherent way to express this, because it just seems so damn obvious. Perhaps wheelchair accessibility is a reasonable parallel: I don’t think I’ve ever heard of anyone complaining about ramps placed around venues to enable wheelchair users to access all parts of an event space. Without said ramps, wheelchair users may not be able to participate, and we all agree that’s a bad thing.
Masking is fundamentally the same. Many people are immunocompromised and a COVID-19 infection is far more likely to be deadly or severely damaging to their health than to those of us fortunate enough not to be high risk. The continuously evolving virus may not, on average, be quite as vicious as it was in 2020, but it’s far from harmless. Vaccines and boosters only go so far, and masking—especially indoors, and especially two-way—has proven fantastically effective at reducing transmission. An event requiring masking is making a choice to inconvenience those who don’t care about it in order to enable those who do care to participate at all.
Masking is merely accessibility ramps for those with invisible conditions.
I’ve debated ad nauseam with myself whether to include this section or not, as I fervently stand behind all of the above regardless of my personal situation, but that’s exactly what provided me with some valuable perspective, so here it is.
My wife’s health has taken a significant turn for the worse as a result of a COVID-19 infection in September of 2022, and she’s now unable to work and is an ambulatory wheelchair user. It’s impossible to predict the exact effects of another infection, but concern is the correct attitude.
As her partner, I make choices with her health in mind, so my risk tolerance for viral infections—and particularly COVID-19—is lower than it likely would be otherwise, and I keep encountering more and more people who are in similar situations. I attended NBPy and PyBay this year because they were each organized as a combination of outdoor space and strong masking policies, and it was great to be back in the community face to face. I had plenty of unmasked conversations outside the enclosed spaces, and that very much felt like the pre-pandemic conference experience.
I want as many people in the Python community as possible to have the same opportunity.
Thanks to my lovely wife Katie for providing feedback on drafts of this post.
Thanks for reading! You can keep up with my writing via the feed or newsletter, or you can get in touch via email or Mastodon.
]]>CMD.exe
. And over the last nearly two decades of Linux and then macOS use, I’ve become pretty devoted to the terminal.
Many different tools have made this journey both possible and delightful, and today I want to call out a few somewhat less known (mostly) non-built-in ones I use fairly regularly, as well as some of the alternatives I think deserve honorable mentions.
For context, I’ve been a full-time macOS user for about a decade and a half at this point, but I’m reasonably sure most of the tools covered here work just fine on Linux, BSD, etc.
I’ve been using kitty for over a year now, and it’s been a pretty great experience. It supports panes, tabs, and multiple windows, can be controlled via command line, and is blazing fast.
Honorable mentions:
These days I’m a rather happy fish shell user, as it’s been a bit friendlier of an experience than other options. My prompt is nice and fast courtesy of Starship, and I just started using Atuin for my shell history and am liking it so far.
Honorable mention goes to tmux, which I don’t currently use as kitty does the trick, but which was an indispensable part of my toolchain for years.
bat is a faster, prettier cat
. Built-in syntax highlighting alone has made it worth installing for me.
broot is an awesome fuzzy directory navigator and file path manipulator. I mostly use it for viewing directory trees and renaming deeply nested files.
eza is a next generation ls
replacement. I’ve aliased l
to it and am generally unaware that it’s what I’m using. (Edit: A previous version of this post referenced exa, which I’ve since learned is unmaintained.)
ripgrep is a next generation, ridiculously fast grep
replacement.
fzf is an awesome command line fuzzy finder. Piping output of, say, ripgrep
to it makes for a lovely interactive search experience.
watch is a classic command repeater. I often start some process and then watch -n .1 ls
in another window to see the progress.
Honorable mentions:
df
alternative. I don’t use it often, but I like the pretty when I do.My primary editor for the past couple of months has been Helix. It’s been working great for me, and I’ve been customizing it slowly and organically. I love how fast it is, and how well It Just Works™.
Perhaps the main highlight is the inversion of Vim’s traditional verb→object order (e.g., d5w
) to the much more natural (to me) object→verb order (e.g., 5wd
). I really appreciate always seeing the selection of text I’m about to change.
There is currently no plugin system, though that will change in the future, but in two months of use I’ve yet to find myself seriously wishing that were already resolved.
Honorable mentions:
The only version control system I use these days is good old Git, and I enhance my use thereof with two other tools.
Firstly, I perform nearly all my Git operations with lazygit. It’s a fantastic terminal UI for Git. I love it so much I nearly always have it open in a dedicated terminal tab for every project I’m actively working on at the time.
Secondly, since all my stuff usually ends up on GitHub, gh makes interacting with repositories from the command line a breeze. I don’t tend to do too much with it, but it’s a handy way of treating issues as to-do lists, for example.
Honorable mention: vimagit was one of my favorite things about using Neovim and Vim.
My favorite API client is Hurl, which is actually a full fledged HTTP client that I tend to use primarily for working with REST APIs. It uses beautifully simple request syntax and offers a few tools for working with responses.
If what I’m getting back is JSON, I tend to use jq to deal with the output, though gron has crept into my workflow as well.
modd has brought me so much joy over the years. It’s a great way to run commands and daemons based on file changes. You can see an example in this post.
From the same developer comes devd, which is just a simple, reliable webserver I sometimes use when working on static web content.
Honorable mentions:
My primary language for many years has been Python. I’ve used more Python related tools than I could possibly keep track of, but two stand out the most.
The first is pipx, which is meant for installing command line Python tools in their own self-managed, isolated virtual environments. For example, I use it to install the Black formatter globally.
The second is Poetry, which I use to manage dependencies for all my projects, as well as build packages for publishing on PyPI.
Honorable mentions:
While I write a lot of small tools for small tasks with frequency, pbclear stands out and gets a decent bit of use. It is my own complement to the macOS built-in pbcopy
and pbpaste
, and all it does is send contents of /dev/null
to said pbcopy
. I don’t entirely know why I’m so insistent on clearing my clipboard all the time, but it makes that easier.
This was a lot of fun to write, honestly. I mostly based it on the output of brew list
, with some pondering about past choices and alternatives.
I also actually learned something: I should use fzf
for a lot more than I do.
Which tools bring you command line joy? Let me know on fediverse/Mastodon or send me an email.
>_
Thanks for reading! You can keep up with my writing via the feed or newsletter, or you can get in touch via email or Mastodon.
]]>I’m pretty good at Python and web stuff, know some things you’d call DevOps (mostly AWS), and could be convinced to learn other things too. I like open source software, the community aspect of the tech industry, and a good developer experience. If that’s enough for you, just email me right away.
I wrote my first lines of code in the early 90s, when I was about 7. Don’t let this fool you, though—what followed was a cool decade and a half of not doing anything noteworthy with computers, until my first real computer-related job in 2006.
At that time I was in early college, rediscovering programming, and I started writing PHP and maintaining Linux servers part-time. Shortly thereafter I went off to get a design degree before going back to programming in 2011.
I then did more PHP than I ever really wanted to, until discovering Python sometime in 2014 or so. I immediately fell in love and dove into it pretty heavily. Ever since, I’ve thought of myself as a Python programmer first and foremost. At first that involved a lot of wrangling Django and Flask backed by PostgreSQL, deployed on Heroku or plain ol’ Linux boxes. Lately I’ve been building distributed systems comprising smaller services and deployed on AWS, thus relying not on web frameworks but the glue available in that ecosystem.
Said ecosystem has exposed me primarily to Lambda, ECS, DynamoDB, EventBridge, SQS, Kinesis, S3, and SES, all managed either by Terraform or CloudFormation.
I’ve also spent a non-trivial portion of the last few years working with TypeScript and Node, though I still wouldn’t label myself an expert with the JavaScript world nearly as comfortably as I might with Python. I’m open to learning and using a variety of other languages and technologies as well.
If you’re looking for my résumé, you can find it here.
You’re most likely a relatively small company, though not necessarily. You don’t cherish process for the sake of process, and prefer a methodology that works, which may very well evolve over time.
You likely rely on Python pretty heavily, and not exclusively for data science/engineering, ops, and/or light scripting. You may also utilize other languages in your stack to a significant degree, as appropriate.
If I’m lucky, you’re heavily invested into open source in some manner. Maybe your main product is open, or you share some ancillary tools with the world. Dare I dream about your developers being allowed (or even encouraged) to contribute back to the software they use for their work?
You support your staff in attending conferences, and perhaps even sponsor some as a way to give back to the community. Ditto for relevant meetups and other local events.
You believe in work-life balance, or however you refer to the idea that happy, well rested employees live better lives and do better work.
You’re not an adtech or defense company, and your products and services aren’t intended to be harmful to anyone. You don’t have contracts with law enforcement agencies, nor plans to enter any.
You’re fully remote and plan on being remote-only or at least remote-first forever. Asynchronous work is either the norm or a possibility.
If the above sections have you feeling like it might be a match, you can email me at nik+hireme@nkantar.com. Do us both a favor and mention this post in the email, so I know you’re not entirely random. ;)
Note to recruiters: I’m not opposed to working with independent recruiters, but I tend to be very selective. If you choose to get in touch, please make sure what you send me makes sense based on the above. If you respect my time, I will respect yours, and quite likely remember you in the future for doing so.
Thanks for reading! You can keep up with my writing via the feed or newsletter, or you can get in touch via email or Mastodon.
]]>If you haven’t yet heard, North Bay Python is an amazing regional conference in Petaluma, California. It’s organized by a wonderful team of awesome people and is consistently one of the most welcoming, inclusive, and empathetic spaces I’ve ever had the privilege of occupying. This was the first installment since the start of the COVID-19 pandemic, and it was a stellar return. The inaugural event inspired me to make PyBeach 2020 happen, and this one may have had a similar effect…
I generally tend to attend conference for the community, but that goes extra for North Bay Python. It’s not because the talks are disappointing—they’re most certainly not—but because the environment at the conference feels like one big, warm, comfy hug. In fact, the talks are part of that, as the presenters are also attendees, and quite likely to be accessible during a break or over a meal. From the organizing team to the other volunteers to the speakers to the attendees, the people of North Bay Python are just great.
A novel addition this year, three cats were in attendance.
Paulina, Poppy, and Pete Tiger were very present, specifically on stage and in the audience. Pearl and Penny passed on the event.
All 16 talks were well worth watching.
PEP talk by Mariatta (video) is an overview of PEPs (Python Enhancement Proposals), covering what they are (and aren’t!), the process they go through, and how we can all participate in this.
I Take Exception to Your Exceptions: Using Custom Errors to Get Your Point Across by Joe Kaufeld (video) is an exploration into creating custom exceptions and using them to effectively communicate issues to the end user/developer.
Teaching with Jupyter by Moshe Zadka (video) is a look at the many ways Jupyter can be used for teaching.
Have you tried... by Paloma Fautley (video) is about failure analysis by way of ocean exploration.
Make Your Engineering Team A Fabulous Place for Programmers with ADHD and Autism by Erin “August” Allard (video) talks about what some folks with Attention Deficit Hyper Disorder and/or Autism Spectrum Disorder might benefit from in their work environment.
Ship your Python code faster with PEX by Shalabh Chaturvedi (video) walks through using PEX to make Docker based deployments more incremental and possibly much less painful.
Back to the Future of Hypermedia in Python by Mario Munoz (video) is a pitch for a return to a slightly old school approach, eschewing heavy JavaScript frontends for a more server oriented stack.
Beyond Programming Paradigms (with Python examples) by Luciano Ramalho (video) suggests thinking about programming languages less in terms of rigid buckets (e.g., object-oriented, functional) and more in terms of features (e.g., functions as first-class objects).
Automate Your City Data with Python by Philip James (video) demonstrates how the power of Python, Datasette, and GitHub Actions can be used to analyze the work of your local city council.
Automated accessibility audits by Pamela Fox (video) teaches some ways of auditing your website for accessibility and automating that process.
Oh the (Methods) You Can (Make): By Dunder Seuss by Joshua D Cannon (video) is 25 minutes of hilarious Python object poetry covering all the magic methods available.
SVGs, Lasers, Reality, and You by Evan Kohilas (video) is a story about making things in the real world with a laser cutter, SVGs, and Python.
Observability For You and Me with OpenTelemetry by Sarah Hudspeth (video) is a trip into the world of understanding what our distributed systems do using OpenTelemetry.
Developing Labs for Teaching Kids Webdev by Matt Cengia (video) is a tale about teaching kids some basic web development and what’s required for doing so.
Two Kinds of Scripting: What Writing Plays Has Taught Me About Writing Python Programs by Marissa Skudlarek (video) compares writing software with writing plays, revealing many similarities.
Catching up on the weird world of LLMs by Simon Willison (video) is one of the more lucid surveys of the current crop of Large Language Models and the undeniable frenzy surrounding them.
With the event having moved from the charming Mystic Theatre in Downtown Petaluma to the equally charming Reis River Ranch a few miles southeast, as well as shifting from early November or December to late July, the logistics of attending were quite different. No longer was sitting through the day an exercise in keeping warm inside an old, un(der?)heated building, but we had to deal with sunny California heat. The actual barn itself was quite manageable, but being outside meant either sticking to the shade or roasting rather quickly.
I discovered that I didn’t really benefit much from bringing a hat, but would’ve been far less comfortable if I hadn’t opted for shorts, sunglasses, and sunscreen. Constant hydration throughout the day was also quite helpful, particularly when I remembered to bring some extra electrolytes on the second day.
Also, the venue had cats.
Petaluma is a quaint little town. I’ve always stayed at Hotel Petaluma and plan on continuing that tradition in the future, as it makes the fun downtown extremely accessible.
Much like in 2019, I chose to ride my motorcycle to the conference. The first time I rode there in one day and back over four, and this time I split each half into two days, which was a good choice. I took the 1/101 in both directions, enjoying lovely California scenery and having a somewhat engaging ride for the most part.
The only truly twisty bit I rode this time around was Skyline Boulevard, which I also took last time and surely will again. The road surface isn’t necessarily the best, but the views are mind blowing.
While not super twisty, good old Pacific Coast Highway rarely disappoints with the views either.
I ended up clocking a bit over 1,000 miles, 100 short of 2019. Not too shabby.
I hope I’ll see you there next year. :)
Thanks for reading! You can keep up with my writing via the feed or newsletter, or you can get in touch via email or Mastodon.
]]>TL;DR: nkantar.com is a Django app exported as a static site.
nkantar.com/ # repo root
├── config/ # Django app settings
├── content/ # content source files: Markdown + assets
├── manage.py # Django management command runner
├── modd.conf # daemon manager used in dev
├── nkantar/ # Django project code
├── output/ # exported static site
├── poetry.lock # Poetry dependency lockfile
├── pyproject.toml # Project config, catering to Poetry
└── scripts/ # deploy.sh, runs at build time
content/
is the most important piece of the puzzle: it’s where Markdown for all posts and pages lives, alongside all static assets used throughout the site.
nkantar.com/content/
├── assets/
│ ├── css/
│ ├── fonts/
│ ├── images/
│ └── media/
├── blog/
│ ├── 2014/
│ │ ├── 08/
│ │ └── ...
│ ├── ...
│ └── 2023/
├── drafts/
├── errors/
├── pages/
└── robots.txt
Much of the structure is probably self-explanatory, but here’s a quick overview anyway:
assets/
gets processed and copied to output/
. Specifically, css/
contains .scss
files that get compiled into one .css
file which is then copied over, and fonts/
, images/
, and media/
are copied verbatim.blog/
directory contains all published posts, split into years and months for easier navigation (and better namespacing).drafts/
directory contains Markdown files of unpublished posts, which are processed in development but not at build time.errors/
contains templates for 404 and 500 errors.pages/
is a directory of Markdown files corresponding to top level pages in the site nav, e.g., About and Now.robots.txt
is a half-hearted attempt to tell search engine crawlers what to do.The Markdown files and static assets have stuck around through a number of different implementations, a testament to the value of using (relatively) plain text for words and self managed file structure for all the accompanying stuff.
The Django app really isn’t all that interesting—it’s mostly just a simple blog with Post
and Page
models, and the simple views and templates you’d expect. There are two things specific to my setup that are worth noting.
One are the management commands I added that import files from content/
into the database, which have a bit of logic to handle renaming, deletion, etc. They allow me to edit the files on disk and have the changes reflected in the web app without having to use the admin panel, touch SQL, or do anything of the sort. This makes it impossible for me to neglect any changes in version control, since the source files show up as modified.
The other is the django-distill plugin, which allows me to easily export the site as a collection of static HTML and assets. It works by wrapping URL patterns in urls.py
files with a function that allows specifying a collection of path arguments that define what should be exported. The plugin then iterates through those arguments, grabs the rendered HTML, and saves it in appropriate files. This is a bit wordy, so here’s an abbreviated example:
from django_distill import distill_path
from nkantar.blog.models import Post
from nkantar.blog.views import PostView
def distill_blog_posts():
posts = [
{
"year": post.year,
"month": post.month_padded,
"slug": post.slug,
}
for post in Post.objects.all()
]
return posts
urlpatterns = [
distill_path(
"<int:year>/<int:month>/<str:slug>/",
PostView.as_view(),
name="blog-post",
distill_func=distill_blog_posts,
),
]
The distill_path
function passes everything through during development, so I get to browse the live Django site as I work on it, with the full power of Django available to me.
The workflows for writing and development overlap a fair bit. As I already mentioned, I run the site locally like any other Django app, and the import scripts take care of content synchronization. But…how? Enter modd, a fantastically useful tool that allows me to run commands and start/stop daemons on file changes based on patterns. So, for a change to a Markdown file I trigger an appropriate import script, for a change to a .scss
file I trigger Sass compilation and copying to the correct static/
location, for a change to a .py
file I restart the Django server, etc. modd.conf
probably illustrates that better than my explanation, so here’s the entire thing:
# Django dev server
**/*.py !**/commands/** {
daemon: ./manage.py runserver
}
# posts
content/blog/** nkantar/blog/management/commands/import_posts.py {
prep: ./manage.py import_posts @mods
}
# drafts
content/drafts/** nkantar/blog/management/commands/import_drafts.py {
prep: ./manage.py import_drafts @mods
}
# pages
content/pages/** nkantar/pages/management/commands/import_pages.py {
prep: ./manage.py import_pages @mods
}
# stylesheets
content/assets/css/** nkantar/core/management/commands/compile_css.py {
prep: ./manage.py compile_css
}
# assets
content/assets/fonts/** content/assets/images/** content/assets/media/** nkantar/core/management/commands/copy_assets.py {
prep: ./manage.py copy_assets
}
Since I use Poetry to manage dependencies, I run poetry run modd
and everything is up and running for me to make whatever changes I need.
This whole thing is currently deployed as a static site on Render. The deployment script goes through the following steps:
output/
directory, just in case.output/
.The result is a fully static site that has minimal overhead and (theoretically) no security risk. 🚀
Thanks for reading! You can keep up with my writing via the feed or newsletter, or you can get in touch via email or Mastodon.
]]>