-
Django
,Python
,Today I Learned
๐ฉ Email, Calendars, and the Chaos of Modern Workflows
I was feeling overloaded with emails this week, and then I remembered that my out-of-office auto-responder told people they should contact me after the first of the year if they needed me to reply.
Thankfully, I could select and archive all of my 2024 emails with this rule
label:inbox after:2023/12/31 before:2025/01/01
, which reconciled my old emails.Calendars and shared Documents
With each Google organization, I’m a member with another Google Calendar, Google Drive, and Google Contacts to manage. That document someone wants feedback on sometimes feels like spinning a wheel, and I need to guess which inbox and account the message might land in.
The best solution that I have found for juggling meeting invites is Reclaim, which is terrific for merging multiple calendars into one calendar so I can at least keep on top of meeting invites and scheduling. Dropbox recently bought them, but I’m hoping that Dropbox will leave them alone.
Email and calendars have become more challenging since I switched to a Mac Studio at the office. While we were returning to work during a blizzard last week, I realized that my personal Mac Mini in my home office had no concept of my work calendar or the 4 or 7 Vivaldi profiles with syncing that I use to jump between orgs all day.
With 1Password, this is a straightforward process to set up and authorize, but it still takes time.
Tonight, I’m pretty sure I even locked myself out of one service because it’s probably not a typical usage pattern to jump between three Macs over two locations with a half dozen profiles to juggle.
Calendar Agent
Over the Thanksgiving break, I wrote my first Calendar Agent, who can read and write to my work calendar. It’s not fully baked yet, but it works well enough to tell me about my upcoming meetings and to create a meeting for me. Sometimes.
The biggest downside to using my Calendar Agent is that I have to run it from my terminal, which isn’t always the most convenient place.
Side note: I might rewrite my agent using PydanticAI as an excuse to learn about the Python agent framework, streamline tool-calling, and play with more local agents using Ollama.
The better email solution
The better email solution was a Django email app called Concorde, one of Adam Fast’s creations. It was Django querysets for managing email rules, which I modified and ran over the years. It quickly created better rules than Gmail supported, like deleting old messages in specific folders after x-days. When I kept my fork running and updated, the tool was invaluable. When I kept my Concorde up and running, my email life was healthier than when I was slower to fix it after an upgrade.
Conclusion
I’m annoyed that the best solutions for these problems are to either pay a company to make a Google Suite usable or you must be a developer to build tools to manage it all.
This stuff sucks.
-
๐ The best NFL broadcast teams and telecast quality, ranked
John Gruber posted his picks a few weeks ago, and I couldn’t disagree more.
2024 NFL broadcast teams and telecast quality, ranked:
- Mike Tirico and Chris Collinsworth, NBC (the best duo by a lot)
- Al Michaels and Kirk Herbstreit, Amazon
- Jim Nantz and Tony Romo, CBS
– the floor –
- Kevin Burkhardt and Tom Brady, Fox
- The Manning Cast. If not for a paint-dryingly bad combo of Joe Buck and Troy Aikman, I would never have put the Manning Cast above anyone. This isn’t even fair to Aikman, who isn’t that bad, but Joe Buck should stick with baseball.
- Joe Buck and Troy Aikman, ABC, aka paint drying
Al Michaels, Mike Tirico, and Chris Collinsworth are what football sounds like to me after Madden. Damn, I miss Madden. Even boringly one-sided games he made special.
-
Django
,Python
,Today I Learned
django-templated-email-md notes aka if you want to format emails with Markdown, use it
I launched Django News Jobs two DjangoCon USs ago, and I somehow put off deploying emails until this week. So, every day or two, I check my Review Jobs queue to see if there’s anything new to approve or reject.
โ Sending emails with Django is straightforward and not very painful.
๐ค Working with most email providers and troubleshooting issues is painful.
๐ค Starting with a blank page and styling emails is painful.
I tried a few third-party apps that I fought with before landing on Jack Linke’s django-templated-email-md (DTEM), which just worked. DTEM doesn’t re-invent the wheel, but it does let me write my email messages with Markdown, which turns out to be all I need.
To add email support, I followed the Usage Guide and then I added one send email function per email that I wanted to send. I’ll eventually refactor this, but it was good enough to get started.
jobs/models.py
For the curious, the code looks like:
# jobs/models.py # a bunch of imports ... from templated_email import send_templated_mail ... class Job(models.Model): ... def send_job_new_email(self): send_templated_mail( template_name="job_new", from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=settings.ADMINS, context={ "job": self, }, )
send_templated_mail
does the actual sending, andtemplate_name
will look for a template file calledjob_new.md
, which will contain our Markdown message.You can put anything you want in
context
, but I will include thejob
so we can include as many details from our job submission as possible.To send a Job, I can call
job.send_job_new_email()
via a signal, cron job, or after someone submits a Job for approval.templates/emails/job_new.md
My emails contain both a “subject” and “content” block, and DTEM figures out the rest for me.
<!-- templates/emails/job_new.md --> {% block subject %}[{{ site_name }}] New Job Posting: {{ job.title }} at {{ job.employer_name }}{% endblock %} {% block content %} # New Job Listing Posted A new job has been posted on the website: - **Title:** {{ job.title }} - **Company:** {{ job.employer_name }} - **Location:** {{ job.location }} - **Remote:** {{ job.get_remote_display }} You can view the full job listing here: <a href="{{ job.get_absolute_url_with_domain }}">{{ job.get_absolute_url_with_domain }}</a> {% endblock content %}
My
get_absolute_url_with_domain
calls in my templates are my workaround for Django’s existential crisis of not making it easy to include the domain name in my urls.Bonus: Django-Q2
I paired DTEM with Django-Q2, my favorite Django task queue. It can work with just the Django ORM, which is good enough for projects like Django News Jobs, which are relatively low traffic but spiky traffic.
If my email-sending provider times out or I have an issue, like my credit card expiring, I never want a user to see it. So, I use a task queue to handle all potentially blocking processes, like sending emails.
Django-Q2 is painless to configure. Using it involves importing
async_task
and modifying oursend_templated_mail
method to be an argument to theasync_task
method.# jobs/models.py # a bunch of imports ... from django_q.tasks import async_task from templated_email import send_templated_mail ... class Job(models.Model): ... def send_job_new_email(self): async_task( send_templated_mail, template_name="job_new", from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=settings.ADMINS, context={ "job": self, }, )
If a Job was sent successfully, I can now check the Django Admin to see if there were any failures.
Forward Email
Now that we have an email confirmed to send, my go-to provider for side projects is Forward Email. They allow outbound SMTP and even support webhooks, which I might write about some other time.
I like them over Mailchimp, Sendmail, and Gmail because they are cheap ($3 a month) and let me have unlimited domains and aliases. I have used them for a dozen side projects for several years now, and they just work. I gave up on Sendmail because I spent more time fighting with them to not turn off my account because the volume was too low. It’s worth $36 a year to have to fight this fight again.
Forward Email’s products and services are fully open-source if you care about such things.
-
Django
,Python
๐ค Rethinking Django's Command-Line Tool: Why We Should Rename `django-admin`
Django has been a key tool for Python web developers for many years. But as new frameworks like FastAPI become prevalent, it’s important to ensure Django stays easy for new and experienced developers. Recently, a discussion thread received over 60 comments about changing Django’s main command from
django-admin
to something else, likedjango
. The thread also explored otherdjango-cmd
possibilities, showcasing many ideas. While the conversation was broad, I want to focus on why renamingdjango-admin
is a good idea.Why Rename
django-admin
?Keep It Simple and Pythonic
When I chaired DjangoCon US, a common question I asked attendees during our opening session was whether they learned Django first or Python first. It surprised me to see the majority of hands raised for “Django first,” which meant that learning Django taught them Python. This showed me how important Django is for teaching Python.
Because Django helps so many people learn Python, it should follow Python’s simple and clean style. Changing
django-admin
todjango
makes the command easier to remember and use. New developers won’t have to remember an extra non-off command, making their experience smoother.Easy Transition for Everyone
One great thing about
django-admin
is that it has been the same for many years. If we rename it todjango
, we don’t have to removedjango-admin
. Instead, we can makedjango
an alias fordjango-admin
. This way, old projects can keep usingdjango-admin
, and new projects can usedjango
. This change won’t disrupt anyone’s work.Real-Life Benefits
This change is prompted by common problems in the Django community. For example, a forum post showed that some developers get confused when trying to run
django-admin
as a Python module. They saw errors likeModuleNotFound
because it wasn’t obvious whypython -m django-admin
didn’t work.Improving Compatibility with Tools Like UV
Another issue is that commands like
uv tool run django
don’t work as expected, but they could. Python’s best practices support usingpython -m django
, which would work smoothly with tools like UV if Django updated its command structure. Instead, the “correct” answer is to runuv tool run --from django django-admin
to bootstrap a Django project.Renaming
django-admin
todjango
and aligning with Python’s module execution standards can make integrating Django with such tools just work. This change would help developers avoid errors and follow best practices, enhancing overall compatibility and workflow.As it exists today, a new user has to learn to use
django-admin
to start a project, and then later, once they learn from seeingpython -m pip
used.Balancing Old and New
Django has been around for a long time and has a strong history. It’s important to keep what makes Django great while also making improvements. Renaming
django-admin
todjango
respects Django’s past and helps it move forward. This change keeps Django reliable for long-time users while improving it for new developers.Is this change zero work?
No, it requires a one-line config change and replacing every instance of
django-admin
in the docs. Any changes in Django’s docs will inevitably trigger translation changes, but these should be small and tightly scoped. As of today, there are 39-page instances to update.Conclusion
Renaming
django-admin
todjango
improves Django’s developer experience. This new name makes the command more straightforward to remember and follows Python’s best practices. It also makes it simpler for new developers to start with modern tools like UV.Although this change means updating some configuration files and documentation, the long-term benefits of having a clearer and more Python-like command are much greater than the initial work needed. Keeping
django-admin
as an alias also ensures that existing projects continue to work without problems.
Join the conversation here and share your ideas on making Django even better for everyone.
PS: This piece was written quickly and proofed even more quickly with Grammarly.
-
Python
๐ My most used commands in my terminal history
This post was inspired by Andrea Grandi’s My ZSH history post, but I modified it back to work with my customized BASH output instead
โ history | awk '{print $4}' | sort | uniq --count | sort --numeric-sort --reverse | head -10 11063 git 7636 just 3280 cd 2575 workon 1512 ls 1061 subl 967 docker 887 cat 703 python 700 gittower
I guess you could say I use
git
a lot.just
is my main workflow driver.workon
- I switched between projects 2575 times.subl
andgittower
are aliases to open Sublime Text and to open the existing project in Git Tower, respectively.I used
docker
more than I would have guessed, butjust
tends to wrap most of that workflow, or it would have givengit
a run for its money. -
Django
,Python
๐ Year in review (short version) - It was a good year
I love end-of-year posts and reading everyone’s posts. Every year, I draft one, and then I give up on publishing it around March or April after a comically long time. I’m going for shorter than longer this year because a lot happened this year, but most important is how I felt closing 2024 out. Overall, 2024 was a good year despite its many ups and downs.
Family
My kids grew a ton; we ended the year with a three and seven-year-old. Three-year-olds have big personalities, and my first-grader decided he could read anything after the first two weeks of school. We had several family trips, including a big family reunion in Chicago(ish) and a trip with my family to Colorado, so we got to see a lot of both families.
Each of our parents went through one hip surgery, and both are doing well. One of my parents had some more serious medical issues that we are hoping are behind everyone now. We buried two aunts in 2024, which brings me down to just one aunt and uncle (both by marriage).
Because time is so short, my new metric of a good year is measured by the health and well-being of our family and parents.
Writing
I published 230 posts this year on my Micro Blog, which ranged from short thoughts to longer posts. I have always wanted to blog, but 2024 is the year I finally made it a priority and figured out what worked for me to ease the publishing friction. I wrote about everything from Python to Django to UV to what I was watching, and I was surprised to see it inspire other blog posts and podcasts. So writing works, friends. Even if it’s just about what show you watched last night.
I’m also pleased with how my Now turned out. The page shows the latest movies, series, and games we are playing. It’s rapidly becoming a dashboard for me, which might be what my homepage turns into.
Conferences
I attended PyCon US in person for the first time since 2019 and DjangoCon US.
Volunteer/Community Work
2024 was a break for me after a decade of leadership roles in several non-profits. After five years on the Python Software Foundation board, 2024 was my first full year off. I stayed in one workgroup, but I stepped down on everything else.
After serving as President or Vice President, 2024 was my first full year as only a Django Events Foundation of North America (DEFNA) director and a DjangoCon US organizer. I had room to volunteer during DjangoCon US without my time being spoken for. It felt more or less like an emeritus role.
I mentored a small cohort for Djangonaut Space this year for the Django Packages project. It was a rewarding process, and I look forward to helping in a future session now that I know what to do. (If you were in my cohort, I promise to post that follow-up blog post soon.)
I joined the Django Software Foundation (DSF) in December, intending to hire an Executive Director. I was so happy to have my time back in 2024 that I decided to give the DSF one more year to do this on their own, but a small mob of friends asked me to run at DjangoCon US this year. So, I ran for the board, and now I have less than two years to hire someone so I can go back to normal life.
Office Hours
I hosted several Office Hours every month, letting them grow into what they needed to be. Some weeks were more structured than others, but watching this turn into a community on its own with lots of new and regular faces joining and sticking around. It’s fun and something I want to keep doing in 2025.
Side Quests
I enjoyed a ton of side quests this year, which I have already written about over the last year. From the newsletter to a job board to even a project to try to make Django talks more discoverable to a bunch of random AI projects. I had a lot of fun working on side projects and side quests.
Going forward
I left a ton of details out, but that’s alright. After 230 posts last year, it’s hard to summarize the journey, but I plan to keep removing the friction that keeps my drafts above 200 posts, which may never see the light of day.
-
โญ GitHub Stars are only good for measuring a project’s number of GitHub Stars, not much else.
Now we can include fake GitHub Stars in “not much else.” Over 3.1 million fake “stars” on GitHub projects used to boost rankings
(via @jonafato)
-
Weeknotes
,Django
,Python
,Today I Learned
๐ Weeknotes for Week 51: December 15 to December 21
I mostly finished my holiday shopping this week. I picked up a few gifts for my family throughout the year and then randomly stressed out last week, thinking I didn’t have enough. I picked up a few last-minute items, and everyone was in good shape outside of a few gift cards. Now, to wrap everything…
Meta
Lately, my weekly notes have been more like monthly notes. I draft them, forget to finish/publish them, and then the following Sunday morning, I start another week. So here we are again.
Family
December is LEGO advent calendar month in my household. Our recently turned three-year-old participates for the first time with the LEGOยฎ ว Disney Advent Calendar 2024, and our seven-year-old picked the LEGOยฎ Star Warsโข Advent Calendar 2024. Even if you are a grownup, these are fun, and you get some cool LEGO mini-figures.
House
If you ask our three-year-old what she wants for Christmas, she defaults to “Rainbow lights” because I didn’t put any lights up outside. Between our Thanksgiving trip to Chicago and two weeks of everyone in the house cycling through the crud, here we are, but at least the Christmas tree got put up.
Community Work
My community cups ran over a bit this week. This is due to my being new to the DSF Board and various end-of-year and before-holidays “things” that pop up.
Community work cut into some of my open-source projects, but I’d like to catch up over the holidays.
I also ended my week with my last Office Hours of 2024. This week was a little too non-profit and community-heavy, so I will balance that better. With the DSF having its own office hours, I want to keep solving those problems in the proper space.
Side projects
- Django News Newsletter: We shipped issue #264.
- Django Packages, Django News Jobs, DjangoTV, and Upgrade Django all had minor updates over the last month. I need to do a better job aka write a tool to help me keep up since that’s mostly watching git commits or an activity feed for the more content-heavy websites.
Side Quests
Writing
2024-12-20:ย ๐๏ธ December 21, 2024, is Volunteer Responsibility Amnesty Dayย
2024-12-19:ย Default Apps 2024ย - Here are my Default Apps 2024, which builds from my Default Apps 2023 post.
2024-12-17:ย ๐คท Why do the Django and Python communities use so many Google Forms?ย
2024-12-14:ย New project to shorten django-admin to django because we are not monstersย - I didn’t realize this idea would kick a hornet’s nest, and yet somehow it did.
Entertainment
We picked up Mighty Morphin Power Rangers: Rita’s Rewind and finished it last week. It was fun, and we like these retro games, but it’s not TMNT’s level of replayability. The game has three hours of gameplay. I’m hoping future updates address this.
I read mixed reviews on G.I. Joe: Wrath of Cobra, but my son was pretty excited about the new Power Rangers game, so we picked it up as a beat ‘em up game we could play over the weekend. It’s buggy in ways that make me wonder how this game has been out for a few months and still has this level of bugs. The controls are bad, but we somehow played through to the last stage before we both shrugged and decided to call it a night. I would give it a two out of five stars type of game. If it weren’t for the nostalgia from my youth, I’d give it one or one and a half stars.
New gear
My NanoKVMs arrived a few weeks ago. I quickly ran out of ports, so I ordered some short HDMI cables, Cat6 cables, more UBC-C cables, and a cheap 8-port switch.
I also lost my Home Assistant machine again, so I swapped out RPis and still ran into issues. As impressive as Home Assistant, running, maintaining, and keeping running is a pain. I have been debating switching to one of their yellow box hardware solutions to support them financially and hoping that I won’t lose my box once a year because it’s so hard to troubleshoot and fix.
I also picked up a 50-foot sewer cam (for looking in walls and vents), an under-desk walking treadmill, and a smart garage door opener to replace our smart Chamberlain garage door opener because they dropped their API.
Next week
This week is Christmas, which means a little bit of travel. Both kids are out of school and preschool, and we both are juggling jobs and deadlines.
-
Django
,Python
๐๏ธ December 21, 2024, is Volunteer Responsibility Amnesty Day
December 21 is Volunteer Responsibility Amnesty Day, one of two days every year that allows you to build out your responsibility inventory and decide what you have time for.
Volunteer Responsibility Amnesty Day is about checking with yourself, and ending the commitments you need to end โ maybe by taking a break, or by rotating it on to someone else, or by sunsetting a project.
It’s one of the best ideas in tech because it creates space to de-commit from projects we might not otherwise have time for, but we feel like we’d be letting people down by stepping back from. It’s a wonderful excuse to step back from projects that have been weighing on your mind.
What I’m stepping down from
This year, I decided it was time to step down from the Paramiko, Fabric, and Invoke Triage Teams because I had not been active there in years. I volunteered to help triage issues that were always a bit over my head.
I joined when Jeff Forcier needed some extra hands, and it might have helped get another maintainer signed up who could help out more. At PyCon US this year, I got to hang out with Jeff. I gave him a heads-up that I felt like I was excess baggage to the project because my good intentions were no longer turning into helping out the project. I have much respect for Jeff (#teamjeff) and all three of these projects.
If you have free cycles and can help with these projects, please check out the open issues and see if you can help. I use all three beneficial projects in one way or another every week.
What you can do
Suppose you are signing up for projects but are not fulfilling your commitment. In that case, I encourage you to use Volunteer Responsibility Amnesty Day as motivation to step down from whatever project or responsibility you may have.
I also want to call out some members of the Python and Django communities. If you are signing up to help with projects you know you won’t have time for, please step back and encourage someone else to help with those projects and roles. If you know that famous face you see plastered on every website and they are friends, please ask them if they know about Volunteer Responsibility Amnesty Day and share your list. That nudge may help them decide if they are taking on too much.
-
Today I Learned
Default Apps 2024
Here are my Default Apps 2024, which builds from my Default Apps 2023 post.
๐ Browser: Vivaldi + Polypane
๐ Search: Kagi
๐ Writing: Obsidian + Grammarly
๐ Cloud File Storage: iCloud Drive + Syncthing
๐ฌ Chat: Apple Messages (family & friends), Discord (friends), Slack (work and community work), Telegram (weird bots) ๐ Calendar: Apple Calendar
๐ Scheduling + Booking: Cal.com
๐น Video Calls: Zoom + Cal.com + Meeter
๐ต Music. : Apple Music + Spotify
๐ค Podcasts: Overcast (boo, hiss) ๐ Password Management: 1Password
๐งโ๐ป Code Editor: Sublime Text + Zed ๐๏ธ Version Control: Tower
๐ Terminal: iTerm2
โ๏ธ VPN: Tailscale + Mullvad ๐ Bookmarks: Raindrop.io
๐ Read It Later: Raindrop.io
๐จ Mail Client: Mimestream (Gmail only and not very often)
๐ฎ Mail Server: Fastmail (I pay for it but still don’t use it enough) + Gmail
๐ Launcher: Alfred 5
๐ผ๏ธ Screenshots: Xnapper
๐ Menu Bar: Ice
๐ค Containers: OrbStack + Docker Compose ๐ Backups: Backblaze โ๏ธ Automation: HammerspoonCommentary
I spent most of 2024 Chrome-free, the new cage-free because I was so frustrated at Mozilla that I deleted Firefox from all my machines. I have doubled my Gmail Accounts and usage between non-profits and community projects.
I’m a happy Vivaldi user and advocate. I only had a handful of issues, quickly solved by turning all or some level of trackers back on. The worst offenders were both my bank and my credit card company.
I dropped Bartender and switched to Ice after the company started acting shady due to poor communication. I have no regrets.
My beloved Overcast podcasting app has been complete garbage for more of 2024 than it was working. The author rewrote it, and it became an unusable disaster for months, one part of Marco’s fault and one part of Apple’s fault for making the iOS App Store such a pain to release products into. It still has Airplay issues and crashes on me, but I can once again play more than one podcast in a row without it crashing. I tried many other podcast apps, and I’m sad that this is the state of things.
I have mostly switched from Docker to OrbStack, which I highly recommend for my development life. I keep Docker around for Compose, but I have several legacy projects I haven’t ported yet.
Previously inspired by: “Apps I’ve been using regularly this year.” Heavily inspired by Matt Stein and cataloged by Robb Knight’s App Defaults
-
Django
,Python
๐คท Why do the Django and Python communities use so many Google Forms?
Last week, I wrote but didn’t publish some notes about why the Django and Python communities use so many Google Forms.
The simple answer is that Google Forms are quick and easy to set up, and data can be easily exported via Google Sheets or a CSV file.
Getting data in and out of Django isn’t hard, but why isn’t it as easy as using Google Forms?
I’d love to see a Django version of Google Forms that we can all use.
Has anyone solved this problem?
From a technical perspective, Django ships with a
JSONField
, which can store any form data we want without having to migrate databases. Turning a flatJSONField
into CSV is already a solved problem.Please note: I am not looking for a Django forms wrapper or your opinions about why Django should use your fancy Django forms tool.
-
Django
,Python
New project to shorten django-admin to django because we are not monsters
One of the biggest mysteries in Django is why I have to run
django-admin
from my terminal instead of just runningdjango
. Confusingly,django-admin
has nothing to do with Django’s admin app.If you have ever wondered why and wanted to type
django
from your terminal, my new project,django-cli-no-admin
solves this problem for you.I looked at several package names on PyPI, including
django-cli
, which I liked the best (someone is name squatting this package.)I gave up and went with
django-cli-no-admin
for lack of a better name.# new school uv pip install django-cli-no-admin # old school pip install django-cli-no-admin # to use it... django --version
This tool aliases Django’s
django-admin
script does but we shorted the name by 50%:[project.scripts] django = "django.core.management:execute_from_command_line"
Should Django adopt this?
Yes. But we can leave
django-admin
alone since we have ~20 years of history referencing it.How long has this lived in your head?
Almost two decades.
Where is the code?
-
Weeknotes
,Django
,Python
,Today I Learned
๐ Weeknotes for Week 48: November 25 to December 1
Family
We drove up to Chicago to see family for five days. It’s a 7.25-hour drive if not stopping were an option, but we usually have to stop at least two or three times to re-fuel, eat, and have a few bathroom breaks. Both of my kids are pro-travelers. Give them an iPad with an offline cache of their favorite Disney and Netflix movies and series, plus some snacks, and they are good to go for 8 to 16 hours. On our last trip back from Omaha, they complained that it was too short because we didn’t stop on our 2.5-hour drive back.
We take turns driving, and through the magic of tethering, I’m surprised that I can comfortably use my laptop from the car for half the trip.
Seeing family was good. There are five kids, ranging from two to nine, but this year everyone is out of diapers, everyone can communicate their needs, and everyone plays together nicely.
We decided to avoid dinner prep drama for Thanksgiving and go out for Mexican food and margaritas. This was an excellent idea. The staff was super friendly, the food was excellent, the margaritas were the right amount of salty and sweet, and everyone got to pick something they enjoyed. There was no food prep or cleanup stress. Overall, our bill for 10 people plus a very generous tip, even after a service fee, was much less than it costs to try to feed 10 people a traditional spread.
Work
It was a short, two-day workweek for me. I helped a client with an inventory project running a week or two behind on their side. The timing wasn’t great because it all landed on my lap the day before I was heading out of town, and it took a few days to run. I hate k8s even more than before.
My main client is missing a tool or two to nail the pulse. I have thought about this because everything takes less time to complete but more time to work on the problem, so things are stretched out. The holiday break was the mental reset I needed to know how to manage this for the next month until our winter break.
Community Work
I skipped all Office Hours this week, but it was a busier few days for my DSF CoC WG card. We reset/rebooted the WG a month ago and have more members and communication. It’s taking more time to reset and settle into a healthy normal.
Side projects
- Django News Newsletter: We shipped issue #260.
- Django News Jobs: More jobs.
- DjangoTV I fixed a bug where non-public videos were being counted in the video counts. I also made several small updates.
Side Quests
Calendar Agent
I spent some free time over the break working on an app that lets ChatGPT control my work calendar. I got the basics up quickly, and my Calendar Agent (Bot?) can look at what’s scheduled on my calendar and create new events for me. The hardest part was navigating Google permissions (I hate them) so that I could access my calendar through Python. Once I got that setup, I used Claude Projects to help me write most of the code.
I ran into an issue with tool calling because I wanted to let the Agent query the time for a day before creating the event. I shifted this logic into my create event function as a fix and realized I was way overthinking it. Now, I have an interesting agent. Still, I have yet to figure out how to run it outside my terminal, which isn’t helpful.
Entertainment
I mostly watched football and basketball this week, but I started a few shows from jefftriplett.com/now/
New gear
Since it was Black Friday, I did some shopping, but not as much as in previous years. I noticed Mastodon vibes are anti-Black Friday, whereas Blue Sky and X are more about saving money and getting a deal. More of my friends shared deals over Slack and Discord than in previous years.
I picked up a five-pack of 32 GB microSD drives for my NanoKVMs, which I hope will be delivered next week. I also bought the family an Air Fryer convection oven combo unit, which may replace our old toaster and be helpful.
Next week
We get to start our LEGO Advent Calendars. I started this tradition with my son when he was two or three. My daughter will be three in a few weeks, so she is now old enough to join in the tradition, too. She requested the Disney Princess calendar, and my son has the Star Wars set. We skipped the traditional set because one can only have so many Santa Clauses.
-
Weeknotes
,Django
๐ Weeknotes for Week 47: November 18 to 24
It’s been months since I published my weeknotes even though I have been drafting them. This week is more about getting back into the habit.
Family
We have hosted family quite a bit over the last month. Our parents have also had two hip replacements (one last week and one three months ago) and a minor (it could have been much, much worse) wreck on their way to see us last weekend. Thankfully, everyone is safe and doing well.
My son is four games into recreation league basketball and had a breakout game this weekend. He was due for one and has been practicing a lot. He’s fun to watch, and sometimes it just clicks that he can take the ball away from someone or dribble in and score. He is also the best passer on his team and can dribble well.
The rest of our family is doing well, but we have been unexpectedly car shopping.
Community Work
2025 DSF Board Election ResultsโI was elected to the Django Software Foundation.
Side projects
- Django News Newsletter: We shipped issue #260.
- Django News Jobs: Small incremental updates.
- DjangoTV Small bug fixes and updates. I created a repo for feedback and, eventually, a roadmap.
Side Quests
My friend Adam let me borrow one of his spare NanoKVM units, which has been a lot of fun. I tested it on my older Intel NUC and an iPad Mini, and both just worked, which surprised me. The device can run Tailscale, too, and serves as a remote hardware KVM that can boot or install a remote machine. I ordered a five-pack of NanoKVMs a few weeks ago because I had a few machines that needed to access that were stuck on boot or other “press any key” prompts before the machines would finish booting.
Entertainment
I will defer to my Now page, which tracks the series, movies, and games I’m watching or playing with my kids. It’s primarily data-driven and automated, and I’m happy with it. I plan to add gear and other data types as I go.
New gear
I got a desk treadmill to relieve stressโinstead, it stressed me out. I picked up one of these desk treadmills for work, but I haven’t gotten a chance to try it out yet. I found an extra-long power cord for it, which will work with my standing desk. I was surprised by how short the cord came.
Next week
It’s Thanksgiving week, and we are out of town to see family.
-
Shows
๐บ A Man on the Inside
I’m about 30 or 40 years too young for A Man on the Inside, but I enjoyed it.
Ted Danson plays “a retired professor gets a new lease on life when a private investigator hires him to go undercover inside a San Francisco retirement home.”
It’s hard to watch a few of the episodes with a dry eye, but it’s worth the journey, and it’s a series I finished in just a few nights.
-
Office Hours
๐ Office Hours for November and December 2024
This Friday, I am hosting office hours, and here is our schedule to finish out 2024.
Please note that on December 13th, I plan to host an earlier morning edition that is friendly for non-US time zones. (Hi, Carlton)
- Friday, November 22nd, 2024,ย 2:00 pm to 4:15 pm - Hard stop for me
- Friday, November 29th, 2024 - No office hours (US Holiday)
- Friday, December 6th, 2024,ย 2:30 pm to 4:30 pm
- Friday, December 13th, 2024,ย 8:00 am to 11:00 am - ๐ Early edition ๐ (updated 12/12 to start an hour earlier)
- Friday, December 20th, 2024,ย 2:30 pm to 4:30 pm
- Friday, December 27th, 2024ย - No office hours (US Holiday)
- Friday, January 3rd, 2025,ย 2:30 pm to 4:30 pm
High-level details
โน๏ธ Anyone can join office hours. Many join because they work remotely, miss seeing faces, and miss the random conversations when a small group hangs out.
โน๏ธ Our office hours are a collaborative space where we can discuss our ongoing projects, catch up, and work together to wrap up our week on a productive note.
๐ As always, everyone is welcome to join, whether you’re a regular attendee or joining for the first time. If you are curious, reach out.
โ If you need any additional details, feel free to send me a message or check out the gist from our previous sessions, where you’ll always find the most recent Zoom link โ ๏ธ
-
UV
๐ฅ The best "Animated This is Fine ZOOM Background" using UV and YT-DLP
We could all use an “Animated This is Fine ZOOM Background” video in times like these.
It’s not obvious how to download a video from YouTube.
I tend to shy away from this outside of this background video explicitly created to be downloaded, so I came up with this one-liner using UV and yt-dlp, which will pull the video.
$ uv run yt-dlp --format=mp4 https://www.youtube.com/watch?v=oEg-9RvcnlY
Hat tip to Maryanne Wachter for finding and recommending this background.
-
Office Hours
๐ Office Hours this Friday and probably next Friday, but lets just take it one day at a time
I wasn’t planning on hosting office hours this week, but given the events of the week, it feels like a damn good way to end the week.
This Friday, November 8th, 2024 from 2:30 pm to 4:30(ish) pm, I am hosting office hours.
The rest of November’s schedule is uncertain, but we’ll figure it out sooner rather than later.
High-level details
โน๏ธ Anyone can join office hours. Many join because they work remotely, miss seeing faces, and miss the random conversations when a small group hangs out.
โน๏ธ Our office hours are a collaborative space where we can discuss our ongoing projects, catch up, and work together to wrap up our week on a productive note.
๐ As always, everyone is welcome to join, whether you’re a regular attendee or joining for the first time. If you are curious, reach out.
โ If you need any additional details, feel free to send me a message or check out the gist from our previous sessions, where you’ll always find the most recent Zoom link โ ๏ธ
-
Python
,UV
,Today I Learned
๐คท UV does everything or enough that I'm not sure what else it needs to do
UV feels like one of those old infomercials where it solves everything, which is where we have landed in the Python world.
I have had several discussions with friends about UV, and even when we talk about it during my weekly(ish) office hours, the list has grown to an ever-growing number of options.
UV started as a quicker way of installing Python packages, and now it’s easier to tell people that UV does everything and to focus on what it doesn’t do.
My favorite feature is that UV can now bootstrap a project to run on a machine that does not previously have Python installed, along with installing any packages your application might require.
Here is my incomplete list of what UV does today:
uv pip install
replaces pip installuv venv
replacespython -m venv
uv pip compile
replaces pip-tools compileuv pip sync
replaces pip-tools syncuv run
replaces pipxuv tool run
replaces pipxuv python
replaces pyenv, asdf, mise, and several other like-minded toolsuv build
- Build your Python package for pypiuv publish
- Upload your Python package to pypiastral-sh/setup-uv
brings UV to GitHub Actionsghcr.io/astral-sh/uv:latest
brings UV and Python to Docker
I copied these four from
uv --help
, which feels like poetry features.uv add
- Add dependencies to the projectuv remove
- Remove dependencies from the projectuv sync
- Update the project’s environmentuv lock
- Update the project’s lockfile
So what doesn’t UV do?
UV does a lot, but it still needs to do everything.
- UV doesn’t run custom scripts defined in our
pyproject.toml
likenpm-run-script
allows. Thank you to @command_tab for jogging my memory. - UV doesn’t convert my non-UV-based projects to UV. Converting is more about prefixing and replacing my commands to switch over.
- UV doesn’t manage, and bump version numbers like the BumpVer, and others do.
- UV doesn’t manage pre-commit like hooks. This is a long shot, but I’d love to see support via
pyproject.toml
. - UV doesn’t replace Python, nor should it.
-
Today I Learned
Please publish and share more
Friends, I encourage you to publish more, indirectly meaning you should write more and then share it.
It’d be best to publish your work in some evergreen space where you control the domain and URL. Then publish on masto-sky-formerly-known-as-linked-don and any place you share and comment on.
You don’t have to change the world with every post. You might publish a quick thought or two that helps encourage someone else to try something new, listen to a new song, or binge-watch a new series.
This week, I have encouraged at least half a dozen people to blog something, and at least three of them were happily surprised to see their work re-posted by another friend or published in a newsletter.
I have nothing against masto-whatever-you-use-this-week or blue-sky-levels-of-vc-money or formerly-called-no-one-cares, but those platforms are hard to share an article on.
So, even if you re-publish to thread your post infinitely, please find a cheap or free publishing platform and own your work. GitHub Pages is a free way to publish your work via GitHub, and they will let you use your own domain name for free.
You don’t need an editor
I used to ask my friends to review my work, and I still sometimes do. Then I realized that 99% of the time, it doesn’t matter.
I pay for Grammarly because I have Dyslexia, and it helps me communicate better. But you don’t have to.
You can use a free tool like LanguageTool, which has an online version that will let you copy and paste your writing into a free, no-login-required Grammar checker. This is more than the average person will do, and it’s a quick and free gut check.
Not every gift needs a bow
Our posts are done when you say they are. You do not have to fret about sticking to landing and having a perfect conclusion. Your posts, like this post, are done after we stop writing.
PS: Write and publish before you write your own static site generator or perfect blogging platform. We have lost billions of good writers to this side quest because they spend all their time working on the platform instead of writing.
-
๐ Even "bad" code is admirable
Some of my favorite projects are what we would consider really “bad” code, but their apps either work or sort of work.
There are no feature branchesโonly a main branch.
They don’t open pull requests or spend time on the dozens or hundreds of open pull requests.
The commit message stream is nothing but “updates” or “fixed” commit subjects.
They leave you feeling that each commitment will be the last before they never touch the project again.
They aren’t playing by any rules because they are only focused on the problem they are working on.
They owe us nothing, and yet they are sharing their work.
The project is only complete after they have scratched the itch and moved on to another project.
-
Justfiles
,Today I Learned
TIL Justfiles can also be Just Scripts
Please note: passing an argument like
--justfile
It only works on MacOS and on Linux.TIL that Justfiles can turn into Just Scripts by adding
#!/usr/bin/env just --justfile
to the top of the file and runningchmod +x
on the file.From the docs:
By adding a shebang line to the top of aย
justfile
ย and making it executable,ยjust
ย can be used as an interpreter for scripts: https://github.com/casey/just?tab=readme-ov-file#just-scriptsjust.sh
#!/usr/bin/env just --justfile @_default: just --justfile just.sh --list @lint *ARGS: uv --quiet run --with pre-commit-uv pre-commit run {{ ARGS }} --all-files
After you run
chmod +x just.sh
, this file may be run using./just.sh
, and sub-commands everythingjust <subcommand>
will just work.Please note that
--justfile just.sh
is needed if you want your Just Script to be able to introspect or call itself.Why?
More and more of my clients are using Justfiles, and occasionally, I want some other recipes that may belong outside the default workflows. These can also be reusable between projects for some of my other internal tooling, so it’s an excellent resource to learn about.
-
Python
,Docker
,UV
,Today I Learned
๐ My notes on publishing a Python package with UV and building a custom GitHub Action for files-to-claude-xml
My new Python application files-to-claude-xml is now on PyPI, which means they are packaged and pip installable. My preferred way of running
files-to-claude-xml
is via UV’s tool run, which will install it if it still needs to be installed and then execute it.$ uv tool run files-to-claude-xml --version
Publishing on PyPi with UV
UV has both build and publish commands, so I took them for a spin today.
uv build
just worked, and a Python package was built.When I tried
uv publish
, it prompted me for some auth settings for which I had to log in to PyPI to create a token.I added those to my local ENV variables I manage with direnv.
export UV_PUBLISH_PASSWORD=<your-PyPI-token-here> export UV_PUBLISH_USERNAME=__token__
Once both were set and registered,
uv publish
published my files on PyPI.GitHub Action
To make
files-to-claude-xml
easier to run on GitHub, I created a custom action to build a_claude.xml
from the GitHub repository.To use this action, I wrote this example workflow, which runs from files-to-claude-xml-example
name: Convert Files to Claude XML on: push jobs: convert-to-xml: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Convert files to Claude XML uses: jefftriplett/files-to-claude-xml-action@main with: files: | README.md main.py output: '_claude.xml' verbose: 'true' - name: Upload XML artifact uses: actions/upload-artifact@v4 with: name: claude-xml path: _claude.xml
My GitHub action is built with a
Dockerfile
, which installsfiles-to-claude-xml
.# Dockerfile FROM ghcr.io/astral-sh/uv:bookworm-slim ENV UV_LINK_MODE=copy RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --frozen --no-install-project WORKDIR /app ENTRYPOINT ["uvx", "files-to-claude-xml"]
To turn a GitHub repository into a runnable GitHub Action, an
action.yml
file needs to exist in the repository. This file describes the input arguments and whichDockerfile
or command to run.# action.yml name: 'Files to Claude XML' description: 'Convert files to XML format for Claude' inputs: files: description: 'Input files to process' required: true type: list output: description: 'Output XML file path' required: false default: '_claude.xml' verbose: description: 'Enable verbose output' required: false default: 'false' version: description: 'Display the version number' required: false default: 'false' runs: using: 'docker' image: 'Dockerfile' args: - ${{ join(inputs.files, ' ') }} - --output - ${{ inputs.output }} - ${{ inputs.verbose == 'true' && '--verbose' || '' }} - ${{ inputs.version == 'true' && '--version' || '' }}
Overall, this works. Claude’s prompting helped me figure it out, which felt fairly satisfying given the goal of
files-to-claude-xml
. -
Gaming
๐ฎ The Legend of Zelda: Echoes of Wisdom review ๐
My oldest kiddo and I finished The Legend of Zelda: Echoes of Wisdom tonight. The 2D Zelda games are some of my favorite games of all time.
I was worried about the game’s overall look. It looked too much like Link’s Awakening, which I wasn’t a fan of compared to the GameBoy version.
I’m happy to say the screenshots don’t do the game justice. They perfected their 2D engine and included some camera control, which helps prevent disappearing into the shadows and behind objects.
The game feels like a sequel to 90s SNES A Link to the Past game with some features from the more recent Switch 3D Zeldas and lore. They even built off of the original Zelda’s 2D dungeon side-scrolling mode in a way that I thought was a lot of fun without dreading it.
While I’m happy to see Zelda finally get her own game, I thought the character had a lot of depth and one of the most interesting sets of powers and echoes, which allows for a dizzyingly large number of monsters and objects that Zelda can conjure on demand.
My only complaint was that I snagged a Lynel within the first few hours of play, but I only had enough power right before the last battle of the game to conjure it.
I’m happy that they thought out post-ending gameplay to make finding everything you missed easier, which was refreshing, instead of being stuck in a weird state.
Echoes of Wisdom might be my favorite 2D(ish) Zelda game and one of my favorite Zelda games.
I hope Nintendo lets Grezzo, the game studio behind it, continue to make weird, 2D Zelda games. This game makes me want to explore the studio’s other Nintendo 3DS Zelda games that I missed out on.
-
Django
,Python
,LLM
๐ค I released files-to-claude-xml and new development workflows
After months of using and sharing this tool via a private gist, I finally carved out some time to release files-to-claude-xml.
Despite my social media timeline declaring LLMs dead earlier today, I have used Claude Projects and Artifacts.
My workflow is to copy a few files into a Claude Project and then create a new chat thread where Claude will help me write tests or build out a few features.
My
files-to-claude-xml
script grew out of some research I did where I stumbled on their Essential tips for long context prompts which documents how to get around some file upload limits which encourages uploading one big file using Claude’s XML-like format.With
files-to-claude-xml
, I build a list of files that I want to import into a Claude Project. Then, I run it to generate a_claude.xml
file, which I drag into Claude. I create a new conversation thread per feature, then copy the finished artifacts out of Claude once my feature or thread is complete.After the feature is complete, I delete the
_claude.xml
file from my project and replace it with an updated copy after I re-runfiles-to-claude-xml
.Features on the go
One bonus of using Claude Projects is that once everything is uploaded, I can use the Claude iOS app as a sort-of notes app and development tool. I can start parallel conversation threads and have it work on new ideas and features. Once I get back to my desktop, I can pull these chat conversations up, and if I like the direction of the feature, I might use them. If not, I have wasted no time or effort on them. This also serves as a nice ToDo list.
New workflows
I am working on side projects further using this methodology. Sometimes, I would like to work on something casually while watching Netflix, but my brain shuts off from coding during the day. Instead of feeling bad that I haven’t added share links to a website or some feature I meant to add last week, I can pair Claude to work on it with me.
I can also get more done with my lunch hours on projects like DjangoTV than I could have otherwise. Overall, I’m happy to have an on-demand assistant to pair with and work on new features and ideas.
It’s also quicker to try out new ideas and projects that I would have needed to make time for.
Alternatives
Simon Willison wrote files-to-prompt, which I think is also worth trying. I contributed to the discussion, feedback, and document structure for the
--cxml
feature.I wrote
files-to-claude-xml
before Simon had cxml support and hoped to not release my version.However, after trying it out on several projects, my ignore/exclude list grew more significant than the files that I wanted to include in my project to send to Claude. I found it easier to generate a list of files to pass to mine instead of maintaining a long list to exclude.