Python on Jeff Triplett's Micro.blog
https://micro.webology.dev/categories/python/
enThu, 26 Feb 2026 20:56:51 -0600How I Use django-simple-nav for Dashboards, Command Palettes, and More
https://micro.webology.dev/2026/02/26/how-i-use-djangosimplenav-for/
Thu, 26 Feb 2026 20:56:51 -0600http://webology.micro.blog/2026/02/26/how-i-use-djangosimplenav-for/<p>I first got exposed to <a href="https://github.com/westerveltco/django-simple-nav">django-simple-nav</a> while working with <a href="https://github.com/joshuadavidthomas">Josh Thomas</a> at <a href="https://github.com/westerveltco">the Westervelt Company</a> over the last two or three years. It quickly became a go-to library in my toolkit. django-simple-nav lets you define nav items and groupings in Python, then hand them off to a Django template to render. I use it for sidebars, headers, dashboards, and other spots where I need a menu.</p>
<p>Since then, I have also started using it on a lot of personal projects. It has been a great fit every time.</p>
<h2 id="defining-nav-items-once-and-reusing-them-everywhere">Defining nav items once and reusing them everywhere</h2>
<p>Recently I wanted to reuse parts of a menu nav without having to update a bunch of different files. I had a dashboard with a sidebar full of links, and I also wanted a command palette that could search across all those same options when you hit Command-K from any page. django-simple-nav was a natural fit because you define your items once in Python and render them with whatever template you want.</p>
<p>Here is an example of what that looks like in Python:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#f92672">from</span> django_simple_nav.nav <span style="color:#f92672">import</span> Nav
<span style="color:#f92672">from</span> django_simple_nav.nav <span style="color:#f92672">import</span> NavGroup
<span style="color:#f92672">from</span> django_simple_nav.nav <span style="color:#f92672">import</span> NavItem
NAV_ITEMS <span style="color:#f92672">=</span> [
NavItem(title<span style="color:#f92672">=</span><span style="color:#e6db74">"Home"</span>, url<span style="color:#f92672">=</span><span style="color:#e6db74">"home"</span>),
NavItem(title<span style="color:#f92672">=</span><span style="color:#e6db74">"Dashboard"</span>, url<span style="color:#f92672">=</span><span style="color:#e6db74">"dashboard"</span>),
NavGroup(
title<span style="color:#f92672">=</span><span style="color:#e6db74">"Admin & Tools"</span>,
items<span style="color:#f92672">=</span>[
NavItem(title<span style="color:#f92672">=</span><span style="color:#e6db74">"Admin"</span>, url<span style="color:#f92672">=</span><span style="color:#e6db74">"admin:index"</span>, permissions<span style="color:#f92672">=</span>[<span style="color:#e6db74">"is_staff"</span>]),
],
),
NavItem(title<span style="color:#f92672">=</span><span style="color:#e6db74">"Login"</span>, url<span style="color:#f92672">=</span><span style="color:#e6db74">"login"</span>),
NavItem(title<span style="color:#f92672">=</span><span style="color:#e6db74">"Logout"</span>, url<span style="color:#f92672">=</span><span style="color:#e6db74">"logout"</span>),
]
<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">DashboardNav</span>(Nav):
template_name <span style="color:#f92672">=</span> <span style="color:#e6db74">"nav/dashboard.html"</span>
items <span style="color:#f92672">=</span> NAV_ITEMS
<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CommandPaletteNav</span>(Nav):
template_name <span style="color:#f92672">=</span> <span style="color:#e6db74">"nav/command_palette.html"</span>
items <span style="color:#f92672">=</span> NAV_ITEMS
</code></pre></div><h2 id="flexible-urls-and-named-routes">Flexible URLs and named routes</h2>
<p>A few things to notice here. When you create a nav item and you have a URL, you can either use the URL’s name or you can give it the path to it and it will figure out what it is. So if I want a nav item for the Django admin, I can use <code>"admin:index"</code> as the URL.</p>
<h2 id="handling-permissions-without-template-logic">Handling permissions without template logic</h2>
<p>Another nice feature is the <code>permissions</code> parameter. The Admin link uses <code>permissions=["is_staff"]</code>, which means it only shows up for staff users. You can use this to hide things based on whether someone is staff, a superuser, or has specific Django permissions. This makes it nice to keep all of your nav options in one place without having to write special conditional logic in your templates to show or hide items for different users.</p>
<h2 id="rendering-a-dashboard-nav-with-django-templates">Rendering a dashboard nav with Django templates</h2>
<p>The other thing to notice is the two Nav classes at the bottom: <code>DashboardNav</code> and <code>CommandPaletteNav</code>. They both use the same <code>NAV_ITEMS</code> list but point to different templates. This is where the reuse really shines.</p>
<p>Here is an example nav template at <code>templates/nav/dashboard.html</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html"><<span style="color:#f92672">nav</span>>
{% for item in items %}
{% if item.items %}
<<span style="color:#f92672">span</span>>{{ item.title }}:
{% for subitem in item.items %}
<<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">"{{ subitem.url }}"</span><span style="color:#960050;background-color:#1e0010">{%</span> <span style="color:#a6e22e">if</span> <span style="color:#a6e22e">subitem</span><span style="color:#960050;background-color:#1e0010">.</span><span style="color:#a6e22e">active</span> <span style="color:#960050;background-color:#1e0010">%}</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">"active"</span><span style="color:#960050;background-color:#1e0010">{%</span> <span style="color:#a6e22e">endif</span> <span style="color:#960050;background-color:#1e0010">%}</span>>
{{ subitem.title }}
</<span style="color:#f92672">a</span>>
{% endfor %}
</<span style="color:#f92672">span</span>>
{% else %}
<<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">"{{ item.url }}"</span><span style="color:#960050;background-color:#1e0010">{%</span> <span style="color:#a6e22e">if</span> <span style="color:#a6e22e">item</span><span style="color:#960050;background-color:#1e0010">.</span><span style="color:#a6e22e">active</span> <span style="color:#960050;background-color:#1e0010">%}</span> <span style="color:#a6e22e">class</span><span style="color:#f92672">=</span><span style="color:#e6db74">"active"</span><span style="color:#960050;background-color:#1e0010">{%</span> <span style="color:#a6e22e">endif</span> <span style="color:#960050;background-color:#1e0010">%}</span>>
{{ item.title }}
</<span style="color:#f92672">a</span>>
{% endif %}
{% endfor %}
</<span style="color:#f92672">nav</span>>
</code></pre></div><p>Then in your base template, you can render it with the template tag:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html">{% load django_simple_nav %}
{% django_simple_nav "myapp.nav.DashboardNav" %}
</code></pre></div><h2 id="building-a-command-palette-from-the-same-nav-items">Building a command palette from the same nav items</h2>
<p>Because the <code>CommandPaletteNav</code> class uses the same <code>NAV_ITEMS</code> list but with a different template, we can render the same nav data as JSON for a command palette. Here is an example template at <code>templates/nav/command_palette.html</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html">{% load django_simple_nav %}
<<span style="color:#f92672">script</span> <span style="color:#a6e22e">type</span><span style="color:#f92672">=</span><span style="color:#e6db74">"application/json"</span> <span style="color:#a6e22e">id</span><span style="color:#f92672">=</span><span style="color:#e6db74">"command-palette-data"</span>>
[{<span style="color:#f92672">%</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">item</span> <span style="color:#66d9ef">in</span> <span style="color:#a6e22e">items</span> <span style="color:#f92672">%</span>}{<span style="color:#f92672">%</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">items</span> <span style="color:#f92672">%</span>}{<span style="color:#f92672">%</span> <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">subitem</span> <span style="color:#66d9ef">in</span> <span style="color:#a6e22e">item</span>.<span style="color:#a6e22e">items</span> <span style="color:#f92672">%</span>}
{<span style="color:#e6db74">"title"</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"{{ subitem.title }}"</span>, <span style="color:#e6db74">"url"</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"{{ subitem.url }}"</span>, <span style="color:#e6db74">"group"</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"{{ item.title }}"</span>}{<span style="color:#f92672">%</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">not</span> <span style="color:#a6e22e">forloop</span>.<span style="color:#a6e22e">last</span> <span style="color:#a6e22e">or</span> <span style="color:#a6e22e">not</span> <span style="color:#a6e22e">forloop</span>.<span style="color:#a6e22e">parentloop</span>.<span style="color:#a6e22e">last</span> <span style="color:#f92672">%</span>},{<span style="color:#f92672">%</span> <span style="color:#a6e22e">endif</span> <span style="color:#f92672">%</span>}{<span style="color:#f92672">%</span> <span style="color:#a6e22e">endfor</span> <span style="color:#f92672">%</span>}{<span style="color:#f92672">%</span> <span style="color:#66d9ef">else</span> <span style="color:#f92672">%</span>}
{<span style="color:#e6db74">"title"</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"{{ item.title }}"</span>, <span style="color:#e6db74">"url"</span><span style="color:#f92672">:</span> <span style="color:#e6db74">"{{ item.url }}"</span>}{<span style="color:#f92672">%</span> <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">not</span> <span style="color:#a6e22e">forloop</span>.<span style="color:#a6e22e">last</span> <span style="color:#f92672">%</span>},{<span style="color:#f92672">%</span> <span style="color:#a6e22e">endif</span> <span style="color:#f92672">%</span>}{<span style="color:#f92672">%</span> <span style="color:#a6e22e">endif</span> <span style="color:#f92672">%</span>}{<span style="color:#f92672">%</span> <span style="color:#a6e22e">endfor</span> <span style="color:#f92672">%</span>}
]
</<span style="color:#f92672">script</span>>
</code></pre></div><p>This also lent itself well when I was trying to do something quickly with Claude. I don’t know how to build a command palette, but I was able to just have Claude create one. I asked it to use HTMX to keep everything pretty lightweight.</p>
<h2 id="final-thoughts">Final thoughts</h2>
<p>All in all, django-simple-nav is a nice library to have in your toolkit. I like using it for headers, menus, dashboard navigation links, and even footer text. If you need flexible, reusable navigation in Django, give it a look.</p>
<hr>
<p>Written by Jeff. Edited with <a href="https://grammarly.com">Grammarly</a> and <a href="https://claude.ai/code">Claude Code</a>.</p>
๐ Office Hours for November
https://micro.webology.dev/2025/11/13/office-hours-for-november/
Thu, 13 Nov 2025 17:08:22 -0600http://webology.micro.blog/2025/11/13/office-hours-for-november/<p>I’m hosting office hours on the next two Fridays in November.</p>
<h2 id="office-hours-schedule">Office Hours Schedule</h2>
<ul>
<li><strong><a href="https://time.is/0230PM_14_November_2025_in_CT">Friday, November 14, 2025, 2:30 pm to 4:30 pm CT</a></strong>
<ul>
<li>Starting at 2pm CT, I’ll be working on some community/open source projects if anyone wants to see Claude Code or Claude Code on the web in action</li>
</ul>
</li>
<li><strong><a href="https://time.is/0230PM_21_November_2025_in_CT">Friday, November 21, 2025, 2:30 pm to 4:30 pm CT</a></strong></li>
<li>๐ฆ <strong>Friday, November 28th - No office hours due to US Thanksgiving</strong></li>
</ul>
<h2 id="join-us">Join Us</h2>
<p>These sessions are open to anyone, especially those working remotely who wants to connect with others. It’s a collaborative space for discussions, catch-ups, or just hanging out while you work.</p>
<p>While we have a bunch of Python and Django developers who regularly attend, anyone is welcome to join even if you don’t use either of these things.</p>
<p>Whether you’re a first-time attendee or a regular, you’re welcome to drop in anytime during the session. No need to stay for the full two hours, come and go as your schedule allows.</p>
<h2 id="how-to-join">How to Join</h2>
<p>The Zoom link is available in the gist from our previous sessions. Feel free to reach out if you need the details.</p>
<h2 id="โน-meeting-guidelines">โน๏ธ Meeting Guidelines</h2>
<ul>
<li>๐ Please mute when someone else is talking ๐</li>
<li>๐ Be respectful to other people and mute when not talking</li>
<li>๐ Be mindful of the spirit of the <a href="https://policies.python.org/python.org/code-of-conduct/">Python Code of Conduct</a></li>
<li>๐ Be mindful and respectful of everyone’s time.</li>
<li>๐ Join because you want to work on something with a small group</li>
<li>โ ๏ธ No marketing your product or service</li>
<li>โ ๏ธ No meeting/recording bots. Humans are welcome. Meeting bots are not.</li>
<li>โ ๏ธ Mute!</li>
</ul>
<p>See you there!</p>
Default Apps 2025
https://micro.webology.dev/2025/10/29/default-apps-here-are-my/
Wed, 29 Oct 2025 17:55:03 -0600http://webology.micro.blog/2025/10/29/default-apps-here-are-my/<p>Here are my Default Apps 2025, which builds from my <a href="https://jefftriplett.com/2024/default-apps-2024/">Default Apps 2024</a> and <a href="https://jefftriplett.com/2023/default-apps-2023/">Default Apps 2023</a> posts.</p>
<ul>
<li>๐ค AI Coding Assistant: <a href="https://claude.ai/code">Claude Code</a> + <a href="https://github.com/features/copilot/cli">GitHub Copilot CLI</a> + <a href="https://openai.com/codex/">Codex</a></li>
<li>โ๏ธ Automation: <a href="https://www.hammerspoon.org">Hammerspoon</a></li>
<li>๐ Backups: <a href="https://www.backblaze.com">Backblaze</a></li>
<li>๐ Bookmarks: <a href="https://raindrop.io">Raindrop.io</a></li>
<li>๐ Browser: <a href="https://vivaldi.com">Vivaldi</a></li>
<li>๐ Calendar: Apple Calendar</li>
<li>๐ฌ Chat: Apple Messages (family & friends), <a href="https://discord.com/">Discord</a> (friends), <a href="https://slack.com/">Slack</a> (work and community work), <a href="https://telegram.org">Telegram</a> (weird bots)</li>
<li>๐ Cloud File Storage: iCloud Drive + <a href="https://syncthing.net">Syncthing</a></li>
<li>๐งโ๐ป Code Editor: <a href="https://www.sublimetext.com/">Sublime Text</a> + <a href="https://zed.dev">Zed</a></li>
<li>๐ค Containers: <a href="https://orbstack.dev">OrbStack</a> + <a href="https://www.docker.com">Docker Compose</a></li>
<li>โ๏ธ Grammar: <a href="https://grammarly.com">Grammarly</a></li>
<li>๐ Launcher: <a href="https://www.alfredapp.com">Alfred 5</a></li>
<li>๐จ Mail Client: <a href="https://mimestream.com">Mimestream</a> (Gmail only and not very often)</li>
<li>๐ฎ Mail Server: <a href="https://www.fastmail.com">Fastmail</a> (I pay for it but still don’t use it enough) + Gmail</li>
<li>๐ Menu Bar: <a href="https://icemenubar.app">Ice</a></li>
<li>๐ต Music: <a href="https://www.apple.com/apple-music/">Apple Music</a> + <a href="https://spotify.com">Spotify</a></li>
<li>๐ Password Management: <a href="https://1password.com/">1Password</a></li>
<li>๐ค Podcasts: Apple Podcast</li>
<li>๐ Read It Later: <a href="https://raindrop.io">Raindrop.io</a></li>
<li>๐ Remote Access: <a href="https://tailscale.com">Tailscale</a></li>
<li>๐๏ธ Scheduling + Booking: <a href="https://cal.com">Cal.com</a></li>
<li>๐ผ๏ธ Screenshots: <a href="https://xnapper.com">Xnapper</a></li>
<li>๐ Search: <a href="https://kagi.com">Kagi</a></li>
<li>๐ Terminal: <a href="https://iterm2.com">iTerm2</a> + <a href="https://ghostty.org">Ghostty</a></li>
<li>๐ Terminal Proxy: <a href="https://vibetunnel.sh">VibeTunnel</a></li>
<li>๐๏ธ Transcription: <a href="https://goodsnooze.gumroad.com/l/macwhisper">MacWhisper</a></li>
<li>๐๏ธ Version Control: <a href="https://www.git-tower.com">Tower</a></li>
<li>๐น Video Calls: <a href="https://zoom.us">Zoom</a> + <a href="https://cal.com">Cal.com</a> + <a href="https://www.trymeeter.com">Meeter</a></li>
<li>โ๏ธ VPN: <a href="https://tailscale.com/mullvad">Mullvad</a></li>
<li>๐ Writing: <a href="https://obsidian.md/">Obsidian</a></li>
</ul>
<h2 id="commentary">Commentary</h2>
<p>The biggest changes for me this year were the addition of more AI tooling like coding assistants, VibeTunnel, and the MacWhisper voice translating app. A year ago, I was writing my own AI coding tools with Python. Today, I use voice-to-text to interact with multiple coding assistants that I can access from any browser connected to my Tailscale Tailnet. It’s a mouthful to explain, but coding looks very different now than it did a year or two ago for me.</p>
<p>I’m not using Google Apps as much as I was a few years ago, though I still need to switch between Google Orgs for work and community organizations.</p>
<h2 id="changes-from-2024">Changes from 2024</h2>
<p><strong>New Additions:</strong></p>
<ul>
<li>AI Coding Assistant (Claude Code, GitHub Copilot CLI, Codex)</li>
<li><strong>VibeTunnel</strong> for accessing my terminal through any browser on my Tailnet</li>
<li>I started using <strong>Ghostty</strong> to run my AI sessions, but I find its lack of find (โf) support to be terribly frustrating.</li>
<li>I have used MacWhisper for years, but now it’s a daily driver. I can hold down the globe (fn) key on my keyboard and talk to my computer and quickly reply to people or even write blog posts with my voice. It’s incredibly cool and powerful.</li>
</ul>
<p><strong>Apps Removed:</strong></p>
<ul>
<li>I dropped <strong>Polypane</strong> (Browser category)</li>
<li>I managed to not use <strong>Overcast</strong> (Podcasts category) for most of the year, but I’m still not happy with Apple’s Podcast app. I might be switching back.</li>
</ul>
<p>Previously inspired by: “Apps I’ve been using regularly this year.” Heavily inspired by <a href="https://mattstein.com/thoughts/default-apps-2023/">Matt Stein</a> and cataloged by Robb Knight’s <a href="https://defaults.rknight.me">App Defaults</a></p>
๐ค On GitHub Copilot CLI and prompts as code
https://micro.webology.dev/2025/10/04/on-github-copilot-cli-and/
Sat, 04 Oct 2025 19:57:19 -0600http://webology.micro.blog/2025/10/04/on-github-copilot-cli-and/<p>I checked out William Vincent’s <a href="https://wsvincent.com/copilot-cli-secret-prompts/">The Secret Prompts in GitHub Copilot CLI</a> tonight, and I wanted to share a few tips and what stood out to me.</p>
<h2 id="github-copilot-cli-uses-claude-sonnet-45-by-default">GitHub Copilot CLI uses Claude Sonnet 4.5 by default</h2>
<blockquote>
<p>No luck other than confirming it is using Claude models by default. Apparently, you can change the underlying model, for example, to ChatGPT 5, by updating the environment variable <code>COPILOT_MODEL=gpt-5</code>; however, we will work with the defaults here.</p>
</blockquote>
<p>You can Copilot CLI’s default model via the <code>/model</code> command and it will also let you pick between three models.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"> Select Model
Choose the AI model to use <span style="color:#66d9ef">for</span> Copilot CLI. The selected model will be persisted and used <span style="color:#66d9ef">for</span> future sessions.
โฏ 1. Claude Sonnet 4.5 <span style="color:#f92672">(</span>default<span style="color:#f92672">)</span> <span style="color:#f92672">(</span>current<span style="color:#f92672">)</span>
2. Claude Sonnet <span style="color:#ae81ff">4</span>
3. GPT-5
4. Cancel <span style="color:#f92672">(</span>Esc<span style="color:#f92672">)</span>
Use โ/โ to navigate, Enter to <span style="color:#66d9ef">select</span>, Esc to cancel
</code></pre></div><p>I’m somewhat surprised that GitHub CLI isn’t shipping with <code>gpt-5-codex</code> model support yet. From my testing, it appears to be OpenAI’s best coding model, but it may be shipping soon.</p>
<h2 id="the-you-are-trick">The “You are” trick</h2>
<blockquote>
<p>Well, what we “really” care about are any prompts that start with “you are” since those are instructions from Copilot CLI to the model.</p>
</blockquote>
<p>The “you are” tip is one I shared with Will and <a href="https://simonwillison.net">Simon Willison</a>, which I have used for a few years when trying to find a tool’s system prompt, which is always buried and obfuscated in their JavaScript files.</p>
<p>Simon and I were invited to Microsoft HQ last month for an AI Insider’s summit (I prefer not to use the term influencer), where we were given early access to GitHub Copilot CLI before the public release. The first thing we both did was dive into the system prompt to see what it contained. Every system prompt has “You are” buried in it somewhere, which makes it much easier to find that paging through 10k lines of JavaScript.</p>
<h2 id="use-xml-tags-in-your-prompts">Use Xml tags in your prompts</h2>
<p>Will posted the Copilot CLI’s system prompt, and one thing that stood out to me was the various XML-tagged sections, such as <code><tips_and_tricks></code> and <code><style></code>, which Anthropic has encouraged for years. They documented the technique in the <a href="https://docs.claude.com/en/docs/build-with-claude/prompt-engineering/use-xml-tags">Use XML tags to structure your prompts</a> section of their docs.</p>
<h2 id="natural-language-prompts-are-becoming-the-universal-programming-interface">Natural language prompts are becoming the universal programming interface</h2>
<p>I have been experimenting with LLMs since the release of GPT-2. While I understand why it pains many developers to admit this, natural language prompts have become a language-agnostic way to program. Somebody could build the same app I’m building using Django with the same prompts in Laravel, Rails, or any other well-documented web framework and underlying programming language.</p>
<p>For many people who are new to development, this is more obvious:</p>
<blockquote>
<p>Prompts are code.</p>
</blockquote>
<p>Languages like Python aren’t going away, but it’s much easier and faster to develop when pairing with an LLM assistant who is always online and can instantly answer any questions that you might have.</p>
<p>The more I examine tools like GitHub Copilot CLI, the clearer it becomes: understanding how these tools prompt their models matters not just for curiosity’s sake, but because knowing how the prompt works helps you work better with the tool. Next time you’re using an AI coding assistant, use the “You are” trick to peek under the hood. You might be surprised by what you find.</p>
๐๏ธ Office Hours for Late August
https://micro.webology.dev/2025/08/21/office-hours-for-late-august/
Thu, 21 Aug 2025 17:54:44 -0600http://webology.micro.blog/2025/08/21/office-hours-for-late-august/<p>I’m hosting office hours on the next two Fridays in August.</p>
<h2 id="office-hours-schedule">Office Hours Schedule</h2>
<ul>
<li><strong><a href="https://time.is/0230PM_22_August_2025_in_CT">Friday, August 22, 2025, 2:30 pm to 4:30 pm CT</a></strong></li>
<li><strong><a href="https://time.is/0230PM_29_August_2025_in_CT">Friday, August 29, 2025, 2:30 pm to 4:30 pm CT</a></strong></li>
</ul>
<h2 id="join-us">Join Us</h2>
<p>These sessions are open to anyone, especially those working remotely who wants to connect with others. It’s a collaborative space for discussions, catch-ups, or just hanging out while you work.</p>
<p>While we have a bunch of Python and Django developers who regularly attend, anyone is welcome to join even if you don’t use either of these things.</p>
<p>Whether you’re a first-time attendee or a regular, you’re welcome to drop in anytime during the session. No need to stay for the full two hours, come and go as your schedule allows.</p>
<h2 id="how-to-join">How to Join</h2>
<p>The Zoom link is available in the gist from our previous sessions. Feel free to reach out if you need the details.</p>
<h2 id="โน-meeting-guidelines">โน๏ธ Meeting Guidelines</h2>
<ul>
<li>๐ Please mute when someone else is talking ๐</li>
<li>๐ Be respectful to other people and mute when not talking</li>
<li>๐ Be mindful of the spirit of the <a href="https://policies.python.org/python.org/code-of-conduct/">Python Code of Conduct</a></li>
<li>๐ Be mindful and respectful of everyone’s time.</li>
<li>๐ Join because you want to work on something with a small group</li>
<li>โ ๏ธ No marketing your product or service</li>
<li>โ ๏ธ No meeting/recording bots. Humans are welcome. Meeting bots are not.</li>
<li>โ ๏ธ Mute!</li>
</ul>
<p>See you there!</p>
Ditching Cloud APIs: How I Set Up Local Text-to-Speech with Kokoro TTS and Python
https://micro.webology.dev/2025/07/02/ditching-cloud-apis-how-i/
Wed, 02 Jul 2025 18:06:57 -0600http://webology.micro.blog/2025/07/02/ditching-cloud-apis-how-i/<p>Today, I fired up the <a href="https://github.com/nazdridoy/kokoro-tts">Voices</a> macOS app, which I occasionally use to convert blog posts or documentation text to audio files that I can take on the go. I usually use one of OpenAI’s Text to speech APIs, but today I noticed there was a new option called Kokoro, which is a local voice model. I was interested in the model and tried out a few voices. Two or three of them were amazing for a local model.</p>
<p>After trying out a few blog posts and listening to them, I decided to find a Python version of the model. It might be slow to run, but at least I can create a pipeline to automate the process of generating voice files from arbitrary text files I have lying around.</p>
<p>That’s when I found the <a href="https://github.com/nazdridoy/kokoro-tts">Kokoro TTS</a> application. I downloaded it and noticed that it needed a few things. Things like NumPy required to be installed, and a couple of things were out of sync, but the project mostly just worked. While it wasn’t quite as fast as a native Mac app, it was still good enough for me to run text files through it very quickly.</p>
<p>Tonight, I decided to write up my notes on how to bootstrap and run the project.</p>
<h2 id="installation">Installation</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">git clone [email protected]:nazdridoy/kokoro-tts.git
cd kokoro-tts
uv add numpy
uv sync
wget https://github.com/nazdridoy/kokoro-tts/releases/download/v1.0.0/voices-v1.0.bin
wget https://github.com/nazdridoy/kokoro-tts/releases/download/v1.0.0/kokoro-v1.0.onnx
</code></pre></div><h2 id="basic-usage">Basic Usage</h2>
<p>Once installed, you can use Kokoro TTS from the command line:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">echo <span style="color:#e6db74">"Hello, this is a test of Kokoro TTS"</span> | ./kokoro-tts /dev/stdin example-streaming.mp3 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span> --format mp3 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span> --voice af_heart
</code></pre></div><p>If you already have an existing text file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">./kokoro-tts textfile.txt example-textfile.mp3 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span> --format mp3 <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span> --voice af_heart
</code></pre></div><h2 id="conclusion">Conclusion</h2>
<p>Kokoro TTS is a solid local text-to-speech option that runs entirely on your machine without requiring API calls or internet connectivity. While it may not be as fast as native Mac apps, the quality is impressive for a local model, and the ability to automate voice file creation from text files makes it valuable for anyone who wants to convert written content to audio format.</p>
<p>One major advantage I discovered is that unlike most cloud APIs like OpenAI’s Text-to-speech that limit posts to around 3000 characters, Kokoro didn’t seem to have an upper limit on any of the content I tried it with. This makes it particularly useful for longer blog posts and documentation.</p>
<p>The setup process is straightforward once you handle the missing dependencies, and the command-line interface makes it easy to integrate into existing workflows or automation scripts.</p>
<hr>
<p><em>Written by Jeff. Edited with <a href="https://grammarly.com">Grammarly</a> and <a href="https://claude.ai/code">Claude Code</a>.</em></p>
๐คทโโ๏ธ I miss Visual Basic
https://micro.webology.dev/2025/05/11/i-miss-visual-basic/
Sun, 11 May 2025 12:33:01 -0600http://webology.micro.blog/2025/05/11/i-miss-visual-basic/<p>I miss Visual Basic. I could build something meaningful with it in 15 to 30 minutes in a way that I have never seen anything since then, even come close to.</p>
<blockquote>
<p>“For example, I personally believe that Visual Basic did more for programming than Object-Oriented Languages did,” Torvalds wrote, “yet people laugh at VB and say it’s a bad language, and they’ve been talking about OO languages for decades. And no, Visual Basic wasn’t a great language, but I think the easy DB interfaces in VB were fundamentally more important than object orientation is, for example.”</p>
</blockquote>
<p><a href="https://retool.com/visual-basic">retool.com/visual-ba…</a></p>
<p>Visual Basic 3 was the first programming language and application building framework that clicked with me. I never loved the language or the inconsistencies, but it just worked. I started with GW Basic, then Turbo Pascal in high school while prepping for college. My high school business teacher let me take an independent study where I made a simple video game in the spirit of Duck Hunt, where the Energizer bunny would explode into bloody bits when you clicked on it. She wasn’t prepared for that, and told me to spend the next half of the year just doing what I was doing.</p>
<p>Visual Basic 6 was the last version I used before Microsoft effectively killed it with .dot by trying to turn it into something it was not. I had already started writing more web/PHP code to avoid Delphi and .net/C# code.</p>
<p>Trying to build a Mac app today makes me sad. There isn’t a good visual way to create an app, which is both the strength and weakness of Visual Basic’s model. When you made a VB application, it looked like a Windows app. Anything I have ever dabbled with Xcode and other application frameworks looks like an unstyled/blank canvas, making me quickly feel fatigued.</p>
<p>I know several people are trying to build one of these apps using Python, but it still feels like the infamous owl drawing, where step #1 is too basic, and the final step is impossible to get to. I’m hoping that AI can help fill in the “how to draw the owl steps,” but my concern is that modern-day frameworks are an impossibly hard bar to maintain and worth it for someone like myself, who isn’t paid to work on them.</p>
<p>I love Python, but I miss Visual Basic.</p>
๐ Announcing The Great Django Webring
https://micro.webology.dev/2025/04/18/announcing-the-great-django-webring/
Fri, 18 Apr 2025 09:50:39 -0600http://webology.micro.blog/2025/04/18/announcing-the-great-django-webring/<p>Blogging is back, so why not bring back Webrings, too?</p>
<p><strong><a href="https://djangowebring.com">The Great Django Webring</a></strong> is a tribute to the early web. It’s a cozy loop of community sites and projects, all linked together jor the joy of discovery.</p>
<p>Join the ring. Explore the community. Rediscover the magic of a smaller, friendlier internet.</p>
๐ค A better Django version/update command and Prompt-driven-development
https://micro.webology.dev/2025/03/26/a-better-django-versionupdate-command/
Wed, 26 Mar 2025 18:27:39 -0600http://webology.micro.blog/2025/03/26/a-better-django-versionupdate-command/<p>Late last year, I wrote <a href="https://github.com/jefftriplett/django-cli-no-admin"><code>django-cli-no-admin,</code></a> a proof-of-concept Python library whose goal was to shorten Django’s default <code>django-admin</code> command to just <code>django</code>.</p>
<p>I published the <a href="https://pypi.org/project/django-cli-no-admin/">package on pypi</a> and it helped create one of the more colorful <a href="https://forum.djangoproject.com/t/name-the-main-command-django/37230">forum topics</a> of 2024. I ended up writing two blog posts about it:</p>
<ul>
<li><a href="https://micro.webology.dev/2024/12/14/new-project-to-shorten-djangoadmin/">New project to shorten django-admin to django because we are not monsters</a></li>
<li><a href="https://micro.webology.dev/2025/01/08/rethinking-djangos-commandline-tool-why/">๐ค Rethinking Django’s Command-Line Tool: Why We Should Rename <code>django-admin</code></a></li>
</ul>
<p>Ever since I had this idea, the idea of building a better <code>django</code> command has stuck with me. While I don’t see Django embracing the idea, of shortening its default command any time soon, that doesn’t mean we shouldn’t</p>
<p>Lately, I have been doing a lot of vibe coding, and I took some inspiration from the <a href="https://github.com/anthropics/claude-code">claude-code</a>’s cli app, which has a nice developer experience (DX). The claude-code app is version-aware and can query npm’s servers to see if a newer version is out and can even update itself.</p>
<p>I took the output of a few of claude-code’s commands and fed that back into Claude and I asked it to build a <code>django</code> command that can mimic this behavior with Django using either pip or UV and the PyPI servers to query for the latest Django version.</p>
<p>Here is what the output of my 2-minute vibe coding sessions resulted in or what I prefer to call Prompt-driven-development (PDD):</p>
<pre tabindex="0"><code>$ django --version
5.1.6
$ django update
Current version: 5.1.6
Checking for updates...
New version available: 5.1.7 (current: 5.1.6)
Installing update...
Successfully updated from 0.5.1.6 to version 0.5.1.7
</code></pre><p>This just worked on the first try. I used a few follow-up prompts to make it less generic and to default to only ever checking Django.</p>
<p>One of my favorite features of prompt-driven development is that I create output like this and give it to Claude or ChatGPT, along with a few libraries with which I want to build a new tool. It gives me the working code back in seconds.</p>
<p>It’s incredibly powerful to focus on the end result without worrying about a clever solution and getting lost along the way. With vibe coding or prompt-driven development, we skip the journey and focus on a better result at our destination.</p>
<h2 id="what-about-funding">What about funding?</h2>
<p>I noticed while updating another npm-based project today that it told me that two projects could be funded.</p>
<p>The Python Packaging User Guide lists <code>sponsor</code> in their <a href="https://packaging.python.org/en/latest/specifications/well-known-project-urls/#well-known-labels">Well-known labels</a> which gives us a data point we could use to list all of the projects that may be funded that we have installed within our Django and/or Python application.</p>
<p>We could also be baked into our <code>django</code> command to determine fundable projects.</p>
<h2 id="thinking-outside-the-box">Thinking outside the box</h2>
<p>Simon Willison’s LLM command also ships with the ability to update itself and even install <a href="https://llm.datasette.io/en/stable/setup.html#installing-plugins">its own plugins</a> by wrapping PIP or UV or whatever magic Simon thought up.</p>
<p>I don’t hate using a DX like <code>django install {package}</code> to install a new package into my application. It would be really cool if we could even add the package into <code>INSTALLED_APPS</code> and make some other suggestion changes if the package could be installed in our urls.py or even explore optional settings that the package might support. I suspect this could even be locally LLM-driven if we wanted to test our docs.</p>
<p>I also recommend you check out Brett Cannon’s <a href="https://github.com/brettcannon/python-launcher">python-launcher</a> app, a Python library wrapper. Brett was writing Rust wrappers to improve Python tooling before it was popular.</p>
๐ค Ideas for "The Levels of Claude" Notes
https://micro.webology.dev/2025/03/05/ideas-for-the-levels-of/
Wed, 05 Mar 2025 18:57:07 -0600http://webology.micro.blog/2025/03/05/ideas-for-the-levels-of/<p>Lately, I have been conversing more with friends, colleagues, clients, and online friends about Claude and coding tools.</p>
<p>It turns out that everyone considers Claude to be something different when they ask, “Have you tried out Claude 3.7 Sonnet?” and that could mean through the website or the mobile app or people who are using it to write code or who are developing against one of Claude’s many REST APIs. Some people primarily interact with Claude by speaking in their voices while walking outside with their pets.</p>
<p>I have even struggled to land at a good starting point, so I have been thinking through the various ways to explain how one might use Claude.</p>
<ul>
<li>You can access Claude Chat through the website.</li>
<li>You can access Claude through the mobile app and use voice support.</li>
<li>You can copy one or more files into Claude.</li>
<li>You can access Claude through an IDE.</li>
<li>You use Claude Projects to organize your projects and might even have a support document you give Claude for more context.</li>
<li>You can access Claude through the Claude Desktop app and use voice support.</li>
<li>You have enabled MCP plugins/servers within the Claude Desktop app.</li>
<li>You can access the Claude Code CLI/terminal app.</li>
<li>You have enabled MCP with the Claude Code CLI/terminal app.</li>
<li>You are writing your own MCP servers in Python or JavaScript.</li>
<li>You have registered for Claude API access and are writing code against one of their REST APIs. (more on this later)</li>
</ul>
<p>I originally broke this down into five levels of Claude, but there were too many platforms and features that blurred together.</p>
<h2 id="accessing-claude">Accessing Claude</h2>
<p>Accessing Claude can be confusing because there are many ways to access it.</p>
<ul>
<li>Web interface</li>
<li>Mobile app</li>
<li>Desktop app</li>
<li>CLI/Terminal app</li>
<li>REST API (could be a third-party app too, that requires a developer token)</li>
</ul>
<h2 id="organization-by-experience-level">Organization by experience level</h2>
<p>If we break these down by user experience or technical proficiency:</p>
<ul>
<li><strong>Beginner</strong>: Web interface, mobile app, IDE integrations</li>
<li><strong>Intermediate</strong>: Desktop app, file uploads, REST API integration</li>
<li><strong>Advanced</strong>: Claude Code CLI</li>
<li><strong>Really Advanced</strong>: You are writing custom MCP implementations.</li>
</ul>
<h2 id="claudes-developer-apis">Claude’s developer APIs</h2>
<p>Claude’s Developer APIs have their vibes and are helpful in various situations. I’m not going to deep dive into them, but from a bird’s eye view:</p>
<ul>
<li>Text Completion API</li>
<li>Embeddings API</li>
<li>Messages API</li>
<li>Claude-in-the-Middle (Tools API and Agents)</li>
<li>Batch API</li>
</ul>
<p>These APIs can also vary based on passing them text, documents, images, or other types of files.</p>
<p>I’m still amazed that I can send an image to Claude and ask it to give me back an HTML file and CSS, and it does a relatively decent job.</p>
<h2 id="conclusion-claude-as-a-spectrum">Conclusion: Claude as a Spectrum</h2>
<p>I wasn’t sure how to end my notes, but I asked Claude to: “Please write a conclusion about Claude being more of a spectrum than just a service.” and this is what it told me:</p>
<blockquote>
<p>When discussing “Claude,” we’re not just talking about a single AI service but rather a spectrum of capabilities, interfaces, and integration points that serve different user needs, technical abilities, and use cases.</p>
</blockquote>
<blockquote>
<p>What makes this spectrum particularly interesting is how it creates different experiences and understanding of what “Claude” actually is. For a casual user accessing the web interface, Claude is simply a helpful chat assistant. For a developer using the API or building MCP servers, Claude becomes an extensible AI platform with programmable capabilities.</p>
</blockquote>
<blockquote>
<p>This spectrum explains why conversations about Claude can sometimes feel like people are talking about entirely different products. In a very real sense, they are. The Claude that helps a writer craft a story through the mobile app exists in a different context than the Claude that helps a developer debug code through an IDE integration.</p>
</blockquote>
<blockquote>
<p>As the Claude ecosystem continues to evolve, this spectrum will likely expand further, with new interfaces, capabilities, and integration points creating even more varied experiences. Understanding Claude as a spectrum rather than a singular service helps contextualize these different experiences and provides a framework for navigating the growing complexity of AI assistants.</p>
</blockquote>
<blockquote>
<p>The most powerful aspect of this spectrum may be that it allows Claude to meet users where they are - from the casual web user to the advanced developer building custom AI solutions. This flexibility transforms Claude from merely an AI service into an adaptable AI ecosystem that can evolve alongside user needs and technical capabilities.</p>
</blockquote>
<p>It’s not half bad.</p>
๐ค "Agents" are Cheat Codes
https://micro.webology.dev/2025/03/02/agents-are-cheat-codes/
Sun, 02 Mar 2025 13:34:36 -0600http://webology.micro.blog/2025/03/02/agents-are-cheat-codes/<p>Lately, I have been trying to wrap my brain around AI Agents, so as a starting point, I have been using <a href="https://ai.pydantic.dev/agents/">Pydantic AI’s Agent</a> class/framework to build “Agents”.</p>
<p>“Agent” is a loaded term. Pydantic AI’s usage is more or less a system prompt and a good API around adding tool calls and working with existing LLMs.</p>
<p>I have written several experimental projects to help me quickly research and find answers to several areas of Django that confuse people, including myself. These ask-one-question bots do their best to fetch the resources they need to answer your questions.</p>
<p>The three I have published publicly are:</p>
<ul>
<li><a href="https://github.com/jefftriplett/django-trademark-agent">Django Trademark Agent</a></li>
<li><a href="https://github.com/jefftriplett/django-bylaws-agent">Django Bylaws Agent</a></li>
<li><a href="https://github.com/jefftriplett/django-dep-10-12-agent">Django DEP-10 and DEP-12 Agent</a></li>
</ul>
<p>None of these are official resources of the Django Software Foundation, nor should they be considered “official” or even “legal” answers to any questions that may arise.</p>
<p>The pattern I landed on for building the system prompts and pulling remote data has been a practical, quick way for me to get feedback and ask questions based on our existing material. I can change a local copy of the bylaws and then ask the Agent questions to see if my potential changes might be comprehensive enough for the Agent to answer.</p>
<p>It effectively feels like running tests on governance to see if the Agent picks up on my changes.</p>
<h2 id="our-cheat-codes">Our Cheat Codes</h2>
<p>These are cheat codes for a quick one-file Agent that one can quickly stand up and ask questions.</p>
<ul>
<li>UV is a cheat code because it can quickly create a one-file Agent with dependencies and the version of Python needed to run the demo baked in.</li>
<li>Pydantic AI’s Agent class is a nice wrapper around a system prompt and can even create a dynamic system prompt. Having a global system prompt has a nice feel to it too.</li>
<li>Pydantic’s <code>BaseModel</code> creates structured data responses as a cheat code for processing unstructured text. If you haven’t seen this pattern yet, you can’t unsee it.</li>
<li>The Jina AI for cleaning up HTML into Markdown is an AI I have wanted for a decade+. I use it in dozens of apps for free, saving me hours of work.</li>
<li>The Python libraries Typer, Rich, and httpx may not seem like they are doing much, and I’m underutilizing them, but their Developer Experience (DX) is great, and they just work.</li>
</ul>
<h2 id="more-areas-to-explore">More areas to explore</h2>
<p>Pydantic AI supports dynamic <a href="https://ai.pydantic.dev/agents/#system-prompts">System Prompts</a>, which might save me a few extra templating steps. They didn’t really click for me before I was writing this post.</p>
<p>When I wrote my Django Agents, I had Pydantic AI’s <a href="https://ai.pydantic.dev/multi-agent-applications/">Multi-agent Applications</a> feature in mind. In theory, I want to ask my Django Agents a question and have it route my question to the appropriate Agent to get an answer.</p>
<p><a href="https://ai.pydantic.dev/tools/">Function Tools</a> or Tool Call is what inspired me to try out Pydantic AI. Function Tools are a way to give LLMs the ability to get information outside of their memory and system prompts when needed. I built one for reading and writing to my work calendar to help me manage my schedule. I didn’t use them for my suite of Django Agents, but when mixed with more real-time data they could be helpful.</p>
<p>We could also refactor each Agent using a reusable tool call so we could assemble one Agent that can gather the information needed to answer common Django Governance questions. I don’t know if that would be effective. In theory, it might not be a bad fit after looking at their <a href="https://github.com/pydantic/pydantic-ai/blob/main/pydantic_ai_slim/pydantic_ai/common_tools/duckduckgo.py">DuckDuckGoSearchTool</a> example.</p>
๐ค My big list of AI/LLM tools, notes, and how I'm using them
https://micro.webology.dev/2025/01/29/my-big-list-of-aillm/
Wed, 29 Jan 2025 18:46:04 -0600http://webology.micro.blog/2025/01/29/my-big-list-of-aillm/<p>I have been using, working, and running commercial and local LLMs for years, but I never got around to sharing the tools and applications I use. Here are some quick notes, tools, and resources I have landed on and use daily.</p>
<h2 id="mac-apps-i-use">Mac Apps I use:</h2>
<p>I do all of my development on Macs. These tools make running local LLMs accessible.</p>
<ul>
<li>
<p><a href="https://ollama.com">Ollama</a> is the server that can download and run 100s of AI models.</p>
</li>
<li>
<p><a href="https://github.com/kevinhermawan/Ollamac">Ollamac</a> is a GUI client for Ollama that lets you write prompts, save history, and allow you to pick models to test out quickly.</p>
</li>
<li>
<p><a href="https://tailscale.com">Tailscale</a> I use Tailscale on all of my devices, which gives me access to my work M2 Mac Studio and home office Mac Mini Pro, which both run Ollama, from anywhere in the world. This makes prototyping at home quick but then I can run a larger model from my work machine and it’s so fast, it feels like the machine is running in my house.</p>
</li>
<li>
<p><a href="https://goodsnooze.gumroad.com/l/openai-bundle">OpenAI Bundle</a>โI bought this bundle because it was the cheapest way to get a bunch of AI apps, including four of Jordi Bruin’s apps. I have used these for a few years.</p>
<ul>
<li><a href="https://goodsnooze.gumroad.com/l/macwhisper">MacWhisper</a> - I use MacWhisper to turn voice notes and podcasts into plain text files for my notes and sometimes blog articles.</li>
<li><a href="https://goodsnooze.gumroad.com/l/voices">Voices</a> - I use Voices when I find a large blog post and want to listen to it while working.</li>
</ul>
</li>
<li>
<p><a href="https://claude.ai/download">Claude for Desktop</a> gets a lot of crap for being “yet another Electron app” instead of a custom-built macOS app, but the people saying that don’t know what they are talking about. The Claude Desktop has voice support and keyboard hotkeys, which make the app incredibly useful. More importantly, Claude Desktop also supports <a href="https://modelcontextprotocol.io">Model Context Protocol</a>, which lets Claude access your file system, git, and anything else you want to access. It’s incredibly powerful, and there’s nothing quite like it.</p>
</li>
</ul>
<h2 id="baseline-rules-for-running-a-model">Baseline rules for running a model</h2>
<p>While running models locally is possible, consumer hardware is constrained by RAM and GPU for even the most miniature models. The easiest mental model to work with is that Billions of parameters are roughly equivalent to your system’s RAM in Gigabytes. An 8B model needs roughly 8G RAM to fit into memory.</p>
<p>My mental formula is somewhat lossy because 40B models fit 32G of memory, and 72B models fit 64G of memory with some room to spare. This is just the rough estimate that I use.</p>
<p>Even though you can run models locally, even the smallest models with a significant context window will exceed your machine’s available RAM. A 128k context window needs about 64 GB of RAM to load into memory for an 8B parameter model fully, even though the model can easily fit into 8GB of RAM. That doesn’t mean the model won’t run locally, but it will run closer than it would if you have more than 72 GB of RAM, which your model fully needs to fit.</p>
<p>I look for three things when I’m evaluating a model:</p>
<ul>
<li>A number of parameters are measured in Billions.</li>
<li>Context length
<ul>
<li>The input context length, which effectively the model’s memory</li>
<li>The output context length, which is how big the answer can be</li>
</ul>
</li>
<li>The type of model:
<ul>
<li><strong>Default Models</strong> are general-purpose models like GPT-4 and Llama 3.3.</li>
<li><strong>Vision Models</strong> and process and read visual data like images and videos.</li>
<li><strong>Tool Models</strong> can call external tools and APIs and perform custom actions to which you give them access.</li>
<li><strong>Embedding Models</strong> can turn text into vectors or tokens, which helps measure your prompts and other RAG operations.</li>
</ul>
</li>
</ul>
<p>What about quantization? Quantization can help you scale a model down so that it might fit into memory, but there’s always a loss in quality, which defeats the purpose of using the bigger model in my book.</p>
<h2 id="keeping-up">Keeping up</h2>
<p>My favorite resource for keeping up is <a href="https://ollama.com/search?o=newest">Ollama’s Models page sorted by Newest</a> models. I check it a few times a day and you’ll see new models release single-digit hours to days before press releases can catch up.</p>
<p>I like <a href="https://www.youtube.com/@technovangelist">Matt Williams'</a> YouTube Channel a lot. It’s the one channel I come back to, and I find that I always learn something from it. His videos tend to be ten to twenty minutes long, which is about right since the material is so dense.</p>
<p>Start with his <a href="https://www.youtube.com/watch?v=QfFRNF5AhME">Optimize Your AI Models</a> videos. They’re a lot to fit in your brain, but they’re a great starting point.</p>
<p><a href="https://simonwillison.net">Simon Willison’s Weblog</a> is good too.</p>
<h2 id="python">Python</h2>
<p>I’ll have to write a few posts on how I’m using LLMs with code, but Simon’s <a href="https://llm.datasette.io/en/stable/">LLM</a> is a good general-purpose AI hammer if you need one.</p>
<p>As of last week, I’m using <a href="https://ai.pydantic.dev">Pydantic AI</a> instead of OpenAI’s or Anthropic’s Python libraries. Pydantic AI will install both of those libraries for you, but I find it to be 100% better and easier to switch between models using it than LangChain (not linked) or anything else I have tried.</p>
Python Click, django-click, and Typer notes
https://micro.webology.dev/2025/01/22/python-click-djangoclick-and-typer/
Wed, 22 Jan 2025 18:37:45 -0600http://webology.micro.blog/2025/01/22/python-click-djangoclick-and-typer/<p>One of the most significant Python innovations in my development toolchain was the <a href="https://click.palletsprojects.com">Click</a> utility, which simplified the creation of Python scripts. Click changed how I approach writing one-off Python scripts and made it easier for me to write better developer experiences around those scripts.</p>
<p>Once I found <a href="https://github.com/GaretJax/django-click">django-click</a>, writing Django management commands was a breeze, using the same Click API that I was already familiar with.</p>
<p>Arguably, the second most significant innovation was the <a href="https://typer.tiangolo.com">Typer</a> library, built on Click, making writing Python scripts even easier. I didn’t think it was possible to be easier than Python Click until Typer came out and proved me wrong.</p>
<h2 id="the-python-click-library">The Python Click library</h2>
<p>A typical pattern I use with Click is creating a command that accepts an argument like a URL.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#f92672">import</span> click
<span style="color:#a6e22e">@click</span><span style="color:#f92672">.</span>command()
<span style="color:#a6e22e">@click</span><span style="color:#f92672">.</span>argument(<span style="color:#e6db74">"url"</span>, type<span style="color:#f92672">=</span>str)
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">command</span>(url):
do_something(url<span style="color:#f92672">=</span>url)
</code></pre></div><h2 id="the-python-django-click-library">The Python django-click library</h2>
<p>This works well, but one improvement would be to accept as many <code>url</code> arguments as I can pass to it. Thankfully, Click solves this with the <code>nargs</code> argument. I always have to look this up, which is why we are here.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#f92672">import</span> djclick <span style="color:#66d9ef">as</span> click
<span style="color:#a6e22e">@click</span><span style="color:#f92672">.</span>command()
<span style="color:#a6e22e">@click</span><span style="color:#f92672">.</span>argument(<span style="color:#e6db74">"urls"</span>, nargs<span style="color:#f92672">=-</span><span style="color:#ae81ff">1</span>, type<span style="color:#f92672">=</span>str)
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">command</span>(urls):
<span style="color:#66d9ef">for</span> url <span style="color:#f92672">in</span> urls:
do_something(url<span style="color:#f92672">=</span>url)
</code></pre></div><h2 id="the-python-typer-library">The Python Typer library</h2>
<p>Suppose we were writing this example with Typer. In that case, we could simplify it to using Python’s native data types, which would make it feel more like I’m writing native code and less like I’m using a library.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#f92672">import</span> typer
app <span style="color:#f92672">=</span> typer<span style="color:#f92672">.</span>Typer()
<span style="color:#a6e22e">@app</span>()
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">command</span>(urls: list[str]):
<span style="color:#66d9ef">for</span> url <span style="color:#f92672">in</span> urls:
do_something(url<span style="color:#f92672">=</span>url)
</code></pre></div><h2 id="conclusion">Conclusion</h2>
<p>There is also a <a href="https://github.com/django-commons/django-typer">django-typer</a> library, bringing the Typer library to Django. I suspect I’ll switch to django-typer the next time I start a new project to give it a good test drive. I can speculate on what that looks like, but I’ll leave that for another day.</p>
๐ฉ Email, Calendars, and the Chaos of Modern Workflows
https://micro.webology.dev/2025/01/15/email-calendars-and-the-chaos/
Wed, 15 Jan 2025 20:51:45 -0600http://webology.micro.blog/2025/01/15/email-calendars-and-the-chaos/<p>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.</p>
<p>Thankfully, I could select and archive all of my 2024 emails with this rule <code>label:inbox after:2023/12/31 before:2025/01/01</code>, which reconciled my old emails.</p>
<h2 id="calendars-and-shared-documents">Calendars and shared Documents</h2>
<p>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.</p>
<p>The best solution that I have found for juggling meeting invites is <a href="https://reclaim.ai">Reclaim</a>, 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.</p>
<p>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.</p>
<p>With 1Password, this is a straightforward process to set up and authorize, but it still takes time.</p>
<p>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.</p>
<h2 id="calendar-agent">Calendar Agent</h2>
<p>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.</p>
<p>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.</p>
<p>Side note: I might rewrite my agent using <a href="https://ai.pydantic.dev">PydanticAI</a> as an excuse to learn about the Python agent framework, streamline tool-calling, and play with more local agents using <a href="https://ollama.com">Ollama</a>.</p>
<h2 id="the-better-email-solution">The better email solution</h2>
<p>The better email solution was a Django email app called Concorde, one of <a href="https://github.com/adamfast">Adam Fast</a>’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.</p>
<h2 id="conclusion">Conclusion</h2>
<p>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.</p>
<p>This stuff sucks.</p>
django-templated-email-md notes aka if you want to format emails with Markdown, use it
https://micro.webology.dev/2025/01/11/djangotemplatedemailmd-notes-aka-if-you/
Sat, 11 Jan 2025 10:22:36 -0600http://webology.micro.blog/2025/01/11/djangotemplatedemailmd-notes-aka-if-you/<p>I launched <a href="https://jobs.django-news.com">Django News Jobs</a> 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.</p>
<p>โ Sending emails with Django is straightforward and not very painful.</p>
<p>๐ค Working with most email providers and troubleshooting issues is painful.</p>
<p>๐ค Starting with a blank page and styling emails is painful.</p>
<p>I tried a few third-party apps that I fought with before landing on <a href="https://jacklinke.com">Jack Linke</a>’s <a href="https://github.com/OmenApps/django-templated-email-md">django-templated-email-md</a> (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.</p>
<p>To add email support, I followed the <a href="https://django-templated-email-md.readthedocs.io/en/latest/usage.html">Usage Guide</a> 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.</p>
<h2 id="jobsmodelspy"><code>jobs/models.py</code></h2>
<p>For the curious, the code looks like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># jobs/models.py</span>
<span style="color:#75715e"># a bunch of imports ... </span>
<span style="color:#f92672">from</span> templated_email <span style="color:#f92672">import</span> send_templated_mail
<span style="color:#f92672">...</span>
<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Job</span>(models<span style="color:#f92672">.</span>Model):
<span style="color:#f92672">...</span>
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">send_job_new_email</span>(self):
send_templated_mail(
template_name<span style="color:#f92672">=</span><span style="color:#e6db74">"job_new"</span>,
from_email<span style="color:#f92672">=</span>settings<span style="color:#f92672">.</span>DEFAULT_FROM_EMAIL,
recipient_list<span style="color:#f92672">=</span>settings<span style="color:#f92672">.</span>ADMINS,
context<span style="color:#f92672">=</span>{
<span style="color:#e6db74">"job"</span>: self,
},
)
</code></pre></div><p><code>send_templated_mail</code> does the actual sending, and <code>template_name</code> will look for a template file called <code>job_new.md</code>, which will contain our Markdown message.</p>
<p>You can put anything you want in <code>context</code>, but I will include the <code>job</code> so we can include as many details from our job submission as possible.</p>
<p>To send a Job, I can call <code>job.send_job_new_email()</code> via a signal, cron job, or after someone submits a Job for approval.</p>
<h2 id="templatesemailsjob_newmd"><code>templates/emails/job_new.md</code></h2>
<p>My emails contain both a “subject” and “content” block, and DTEM figures out the rest for me.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html"><span style="color:#75715e"><!-- templates/emails/job_new.md --></span>
{% 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: <<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">"{{ job.get_absolute_url_with_domain }}"</span>>{{ job.get_absolute_url_with_domain }}</<span style="color:#f92672">a</span>>
{% endblock content %}
</code></pre></div><p>My <code>get_absolute_url_with_domain</code> 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.</p>
<h2 id="bonus-django-q2">Bonus: Django-Q2</h2>
<p>I paired DTEM with <a href="https://github.com/django-q2/django-q2">Django-Q2</a>, 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.</p>
<p>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.</p>
<p>Django-Q2 is painless to configure. Using it involves importing <code>async_task</code> and modifying our <code>send_templated_mail</code> method to be an argument to the <code>async_task</code> method.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># jobs/models.py</span>
<span style="color:#75715e"># a bunch of imports ... </span>
<span style="color:#f92672">from</span> django_q.tasks <span style="color:#f92672">import</span> async_task
<span style="color:#f92672">from</span> templated_email <span style="color:#f92672">import</span> send_templated_mail
<span style="color:#f92672">...</span>
<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Job</span>(models<span style="color:#f92672">.</span>Model):
<span style="color:#f92672">...</span>
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">send_job_new_email</span>(self):
async_task(
send_templated_mail,
template_name<span style="color:#f92672">=</span><span style="color:#e6db74">"job_new"</span>,
from_email<span style="color:#f92672">=</span>settings<span style="color:#f92672">.</span>DEFAULT_FROM_EMAIL,
recipient_list<span style="color:#f92672">=</span>settings<span style="color:#f92672">.</span>ADMINS,
context<span style="color:#f92672">=</span>{
<span style="color:#e6db74">"job"</span>: self,
},
)
</code></pre></div><p>If a Job was sent successfully, I can now check the Django Admin to see if there were any failures.</p>
<h2 id="forward-email">Forward Email</h2>
<p>Now that we have an email confirmed to send, my go-to provider for side projects is <a href="https://forwardemail.net">Forward Email</a>. They allow outbound SMTP and even support webhooks, which I might write about some other time.</p>
<p>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.</p>
<p>Forward Email’s products and services are fully open-source if you care about such things.</p>
๐ค Rethinking Django's Command-Line Tool: Why We Should Rename `django-admin`
https://micro.webology.dev/2025/01/08/rethinking-djangos-commandline-tool-why/
Wed, 08 Jan 2025 19:15:02 -0600http://webology.micro.blog/2025/01/08/rethinking-djangos-commandline-tool-why/<p>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 <a href="https://forum.djangoproject.com/t/name-the-main-command-django/37230">discussion thread</a> received over 60 comments about changing Django’s main command from <code>django-admin</code> to something else, like <code>django</code>. The thread also explored other <code>django-cmd</code> possibilities, showcasing many ideas. While the conversation was broad, I want to focus on why renaming <code>django-admin</code> is a good idea.</p>
<h2 id="why-rename-django-admin">Why Rename <code>django-admin</code>?</h2>
<h3 id="keep-it-simple-and-pythonic">Keep It Simple and Pythonic</h3>
<p>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.</p>
<p>Because Django helps so many people learn Python, it should follow Python’s simple and clean style. Changing <code>django-admin</code> to <code>django</code> makes the command easier to remember and use. New developers won’t have to remember an extra non-off command, making their experience smoother.</p>
<h3 id="easy-transition-for-everyone">Easy Transition for Everyone</h3>
<p>One great thing about <code>django-admin</code> is that it has been the same for many years. If we rename it to <code>django</code>, we don’t have to remove <code>django-admin</code>. Instead, we can make <code>django</code> an alias for <code>django-admin</code>. This way, old projects can keep using <code>django-admin</code>, and new projects can use <code>django</code>. This change won’t disrupt anyone’s work.</p>
<h2 id="real-life-benefits">Real-Life Benefits</h2>
<p>This change is prompted by common problems in the Django community. For example, a <a href="https://forum.djangoproject.com/t/running-django-admin-as-a-python-module/36401">forum post</a> showed that some developers get confused when trying to run <code>django-admin</code> as a Python module. They saw errors like <code>ModuleNotFound</code> because it wasn’t obvious why <code>python -m django-admin</code> didn’t work.</p>
<h3 id="improving-compatibility-with-tools-like-uv">Improving Compatibility with Tools Like UV</h3>
<p>Another issue is that commands like <code>uv tool run django</code> don’t work as expected, but they could. Python’s best practices support using <code>python -m django</code>, which would work smoothly with tools like UV if Django updated its command structure. Instead, the “correct” answer is to run <code>uv tool run --from django django-admin</code> to bootstrap a Django project.</p>
<p>Renaming <code>django-admin</code> to <code>django</code> 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.</p>
<p>As it exists today, a new user has to learn to use <code>django-admin</code> to start a project, and then later, once they learn from seeing <code>python -m pip</code> used.</p>
<h2 id="balancing-old-and-new">Balancing Old and New</h2>
<p>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 <code>django-admin</code> to <code>django</code> respects Django’s past and helps it move forward. This change keeps Django reliable for long-time users while improving it for new developers.</p>
<h2 id="is-this-change-zero-work">Is this change zero work?</h2>
<p>No, it requires a <a href="https://micro.webology.dev/2024/12/14/new-project-to-shorten-djangoadmin/">one-line config change</a> and replacing every instance of <code>django-admin</code> 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 <a href="https://docs.djangoproject.com/en/5.1/search/?q=%22django-admin%22">39-page instances</a> to update.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Renaming <code>django-admin</code> to <code>django</code> 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.</p>
<p>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 <code>django-admin</code> as an alias also ensures that existing projects continue to work without problems.</p>
<hr>
<p>Join the conversation <a href="https://forum.djangoproject.com/t/name-the-main-command-django/37230">here</a> and share your ideas on making Django even better for everyone.</p>
<p>PS: This piece was written quickly and proofed even more quickly with Grammarly.</p>
๐ My most used commands in my terminal history
https://micro.webology.dev/2025/01/02/my-most-used-commands-in/
Thu, 02 Jan 2025 09:02:42 -0600http://webology.micro.blog/2025/01/02/my-most-used-commands-in/<p>This post was inspired by <a href="https://mastodon.social/@andreagrandi">Andrea Grandi’s</a> <a href="https://www.andreagrandi.it/posts/my-zsh-history/">My ZSH history</a> post, but I modified it back to work with my customized BASH output instead</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">โ history | awk <span style="color:#e6db74">'{print $4}'</span> | sort | uniq --count | sort --numeric-sort --reverse | head -10
<span style="color:#ae81ff">11063</span> git
<span style="color:#ae81ff">7636</span> just
<span style="color:#ae81ff">3280</span> cd
<span style="color:#ae81ff">2575</span> workon
<span style="color:#ae81ff">1512</span> ls
<span style="color:#ae81ff">1061</span> subl
<span style="color:#ae81ff">967</span> docker
<span style="color:#ae81ff">887</span> cat
<span style="color:#ae81ff">703</span> python
<span style="color:#ae81ff">700</span> gittower
</code></pre></div><p>I guess you could say I use <code>git</code> a lot.</p>
<p><code>just</code> is my main workflow driver.</p>
<p><code>workon</code> - I switched between projects 2575 times.</p>
<p><code>subl</code> and <code>gittower</code> are aliases to open <a href="https://www.sublimetext.com">Sublime Text</a> and to open the existing project in <a href="https://www.git-tower.com">Git Tower</a>, respectively.</p>
<p>I used <code>docker</code> more than I would have guessed, but <code>just</code> tends to wrap most of that workflow, or it would have given <code>git</code> a run for its money.</p>
๐ Year in review (short version) - It was a good year
https://micro.webology.dev/2025/01/01/year-in-review-short-version/
Wed, 01 Jan 2025 22:09:10 -0600http://webology.micro.blog/2025/01/01/year-in-review-short-version/<p>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.</p>
<h2 id="family">Family</h2>
<p>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.</p>
<p>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).</p>
<p>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.</p>
<h2 id="writing">Writing</h2>
<p>I published <a href="https://micro.webology.dev/archive/">230 posts</a> this year on my <a href="https://micro.webology.dev">Micro Blog</a>, 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.</p>
<p>I’m also pleased with how my <a href="https://jefftriplett.com/now/">Now</a> 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.</p>
<h2 id="conferences">Conferences</h2>
<p>I attended PyCon US in person for the first time since 2019 and DjangoCon US.</p>
<h2 id="volunteercommunity-work">Volunteer/Community Work</h2>
<p>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.</p>
<p>After serving as President or Vice President, 2024 was my first full year as <strong>only</strong> 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.</p>
<p>I mentored a small cohort for Djangonaut Space this year for the <a href="https://djangopackages.org">Django Packages</a> 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.)</p>
<p>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.</p>
<h2 id="office-hours">Office Hours</h2>
<p>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.</p>
<h2 id="side-quests">Side Quests</h2>
<p>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.</p>
<h2 id="going-forward">Going forward</h2>
<p>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.</p>
๐ Weeknotes for Week 51: December 15 to December 21
https://micro.webology.dev/2024/12/22/weeknotes-for-week-december-to/
Sun, 22 Dec 2024 21:34:07 -0600http://webology.micro.blog/2024/12/22/weeknotes-for-week-december-to/<p>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…</p>
<h2 id="meta">Meta</h2>
<p>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.</p>
<h2 id="family">Family</h2>
<p>December is LEGO advent calendar month in my household. Our recently turned three-year-old participates for the first time with the <a href="https://www.lego.com/en-us/product/advent-calendar-2024-43253">LEGOยฎ ว Disney Advent Calendar 2024</a>, and our seven-year-old picked the <a href="https://www.lego.com/en-us/product/lego-star-wars-advent-calendar-2024-75395">LEGOยฎ Star Warsโข Advent Calendar 2024</a>. Even if you are a grownup, these are fun, and you get some cool LEGO mini-figures.</p>
<h2 id="house">House</h2>
<p>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.</p>
<h2 id="community-work">Community Work</h2>
<p>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.</p>
<p>Community work cut into some of my open-source projects, but I’d like to catch up over the holidays.</p>
<p>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.</p>
<h2 id="side-projects">Side projects</h2>
<ul>
<li><strong><a href="https://django-news.com">Django News Newsletter</a>:</strong> We shipped issue <a href="https://django-news.com/issues/264#start">#264</a>.</li>
<li><a href="https://djangopackages.org">Django Packages</a>, <a href="https://jobs.django-news.com">Django News Jobs</a>, <a href="https://djangotv.com">DjangoTV</a>, and <a href="https://upgradedjango.com">Upgrade Django</a> 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.</li>
</ul>
<h2 id="side-quests">Side Quests</h2>
<h2 id="writing">Writing</h2>
<p><a href="https://micro.webology.dev/2024/12/20/december-is-volunteer.html">2024-12-20</a>:ย <strong>๐๏ธ December 21, 2024, is Volunteer Responsibility Amnesty Day</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/12/19/default-apps.html">2024-12-19</a>:ย <strong>Default Apps 2024</strong>ย - Here are my Default Apps 2024, which builds from my Default Apps 2023 post.</p>
<p><a href="https://micro.webology.dev/2024/12/17/why-do-the.html">2024-12-17</a>:ย <strong>๐คท Why do the Django and Python communities use so many Google Forms?</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/12/14/new-project-to.html">2024-12-14</a>:ย <strong>New project to shorten django-admin to django because we are not monsters</strong>ย - I didn’t realize this idea would kick a hornet’s nest, and yet somehow it did.</p>
<h2 id="entertainment">Entertainment</h2>
<p>We picked up <a href="https://www.backloggd.com/games/mighty-morphin-power-rangers-ritas-rewind/">Mighty Morphin Power Rangers: Rita’s Rewind</a> 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.</p>
<p>I read mixed reviews on <a href="https://www.backloggd.com/games/gi-joe-wrath-of-cobra/">G.I. Joe: Wrath of Cobra</a>, 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.</p>
<h2 id="new-gear">New gear</h2>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<h2 id="next-week">Next week</h2>
<p>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.</p>
๐๏ธ December 21, 2024, is Volunteer Responsibility Amnesty Day
https://micro.webology.dev/2024/12/20/december-is-volunteer-responsibility-amnesty/
Fri, 20 Dec 2024 13:43:01 -0600http://webology.micro.blog/2024/12/20/december-is-volunteer-responsibility-amnesty/<p>December 21 is <a href="https://www.volunteeramnestyday.net">Volunteer Responsibility Amnesty Day</a>, one of two days every year that allows you to build out your responsibility inventory and decide what you have time for.</p>
<blockquote>
<p>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.</p>
</blockquote>
<p>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.</p>
<h2 id="what-im-stepping-down-from">What I’m stepping down from</h2>
<p>This year, I decided it was time to step down from the <a href="https://github.com/orgs/paramiko">Paramiko</a>, <a href="https://github.com/fabric">Fabric</a>, and <a href="https://github.com/pyinvoke">Invoke</a> 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.</p>
<p>I joined when <a href="https://github.com/bitprophet">Jeff Forcier</a> 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.</p>
<p>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.</p>
<h2 id="what-you-can-do">What you can do</h2>
<p>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.</p>
<p>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.</p>
๐คท Why do the Django and Python communities use so many Google Forms?
https://micro.webology.dev/2024/12/17/why-do-the-django-and/
Tue, 17 Dec 2024 11:09:32 -0600http://webology.micro.blog/2024/12/17/why-do-the-django-and/<p>Last week, I wrote but didn’t publish some notes about why the Django and Python communities use so many Google Forms.</p>
<p>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.</p>
<p>Getting data in and out of Django isn’t hard, but why isn’t it as easy as using Google Forms?</p>
<p>I’d love to see a Django version of Google Forms that we can all use.</p>
<p>Has anyone solved this problem?</p>
<p>From a technical perspective, Django ships with a <code>JSONField</code>, which can store any form data we want without having to migrate databases. Turning a flat <code>JSONField</code> into CSV is already a solved problem.</p>
<p><strong>Please note:</strong> I am not looking for a Django forms wrapper or your opinions about why Django should use your fancy Django forms tool.</p>
New project to shorten django-admin to django because we are not monsters
https://micro.webology.dev/2024/12/14/new-project-to-shorten-djangoadmin/
Sat, 14 Dec 2024 21:18:43 -0600http://webology.micro.blog/2024/12/14/new-project-to-shorten-djangoadmin/<p>One of the biggest mysteries in Django is why I have to run <code>django-admin</code> from my terminal instead of just running <code>django</code>. Confusingly, <code>django-admin</code> has nothing to do with Django’s admin app.</p>
<p>If you have ever wondered why and wanted to type <code>django</code> from your terminal, my new project, <code>django-cli-no-admin</code> solves this problem for you.</p>
<p>I looked at several package names on <a href="https://pypi.org">PyPI</a>, including <code>django-cli</code>, which I liked the best (someone is name squatting this package.)</p>
<p>I gave up and went with <a href="https://pypi.org/project/django-cli-no-admin/"><code>django-cli-no-admin</code></a> for lack of a better name.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#75715e"># new school</span>
uv pip install django-cli-no-admin
<span style="color:#75715e"># old school</span>
pip install django-cli-no-admin
<span style="color:#75715e"># to use it...</span>
django --version
</code></pre></div><p>This tool aliases Django’s <code>django-admin</code> script does but we shorted the name by 50%:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-toml" data-lang="toml">[<span style="color:#a6e22e">project</span>.<span style="color:#a6e22e">scripts</span>]
<span style="color:#a6e22e">django</span> = <span style="color:#e6db74">"django.core.management:execute_from_command_line"</span>
</code></pre></div><h2 id="should-django-adopt-this">Should Django adopt this?</h2>
<p>Yes. But we can leave <code>django-admin</code> alone since we have ~20 years of history referencing it.</p>
<h2 id="how-long-has-this-lived-in-your-head">How long has this lived in your head?</h2>
<p>Almost two decades.</p>
<h2 id="where-is-the-code">Where is the code?</h2>
<p><a href="https://github.com/jefftriplett/django-cli-no-admin">https://github.com/jefftriplett/django-cli-no-admin</a></p>
๐ Weeknotes for Week 48: November 25 to December 1
https://micro.webology.dev/2024/12/01/weeknotes-for-week-november-to/
Sun, 01 Dec 2024 17:48:17 -0600http://webology.micro.blog/2024/12/01/weeknotes-for-week-november-to/<h2 id="family">Family</h2>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<h2 id="work">Work</h2>
<p>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.</p>
<p>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.</p>
<h2 id="community-work">Community Work</h2>
<p>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.</p>
<h2 id="side-projects">Side projects</h2>
<ul>
<li><strong><a href="https://django-news.com">Django News Newsletter</a>:</strong> We shipped issue <a href="https://django-news.com/issues/260#start">#260</a>.</li>
<li><a href="https://jobs.django-news.com">Django News Jobs</a>: More jobs.</li>
<li><a href="https://djangotv.com">DjangoTV</a> I fixed a bug where non-public videos were being counted in the video counts. I also made several small updates.</li>
</ul>
<h2 id="side-quests">Side Quests</h2>
<h3 id="calendar-agent">Calendar Agent</h3>
<p>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.</p>
<p>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.</p>
<h2 id="entertainment">Entertainment</h2>
<p>I mostly watched football and basketball this week, but I started a few shows from <a href="https://jefftriplett.com/now/">jefftriplett.com/now/</a></p>
<h2 id="new-gear">New gear</h2>
<p>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.</p>
<p>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.</p>
<h2 id="next-week">Next week</h2>
<p>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.</p>
๐คท UV does everything or enough that I'm not sure what else it needs to do
https://micro.webology.dev/2024/11/03/uv-does-everything-or-enough/
Sun, 03 Nov 2024 19:36:23 -0600http://webology.micro.blog/2024/11/03/uv-does-everything-or-enough/<p>UV feels like one of those old infomercials where it solves everything, which is where we have landed in the Python world.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>Here is my incomplete list of what UV does today:</p>
<ul>
<li><code>uv pip install</code> replaces pip install</li>
<li><code>uv venv</code> replaces <code>python -m venv</code></li>
<li><code>uv pip compile</code> replaces pip-tools compile</li>
<li><code>uv pip sync</code> replaces pip-tools sync</li>
<li><code>uv run</code> replaces pipx</li>
<li><code>uv tool run</code> replaces pipx</li>
<li><code>uv python</code> replaces pyenv, asdf, mise, and several other like-minded tools</li>
<li><code>uv build</code> - Build your Python package for pypi</li>
<li><code>uv publish</code> - Upload your Python package to pypi</li>
<li><code>astral-sh/setup-uv</code> brings UV to GitHub Actions</li>
<li><code>ghcr.io/astral-sh/uv:latest</code> brings UV and Python to Docker</li>
</ul>
<p>I copied these four from <code>uv --help</code>, which feels like poetry features.</p>
<ul>
<li><code>uv add</code> - Add dependencies to the project</li>
<li><code>uv remove</code> - Remove dependencies from the project</li>
<li><code>uv sync</code> - Update the project’s environment</li>
<li><code>uv lock</code> - Update the project’s lockfile</li>
</ul>
<h2 id="so-what-doesnt-uv-do">So what doesn’t UV do?</h2>
<p>UV does a lot, but it still needs to do everything.</p>
<ul>
<li>UV doesn’t run custom scripts defined in our <code>pyproject.toml</code> like <a href="https://docs.npmjs.com/cli/v10/commands/npm-run-script"><code>npm-run-script</code></a> allows. Thank you to <a href="https://mastodon.social/@[email protected]/113422203525133302">@command_tab</a> for jogging my memory.</li>
<li>UV doesn’t convert my non-UV-based projects to UV. Converting is more about prefixing and replacing my commands to switch over.</li>
<li>UV doesn’t manage, and bump version numbers like the <a href="https://github.com/mbarkhau/bumpver">BumpVer</a>, and others do.</li>
<li>UV doesn’t manage <a href="https://micro.webology.dev/">pre-commit</a> like hooks. This is a long shot, but I’d love to see support via <code>pyproject.toml</code>.</li>
<li>UV doesn’t replace Python, nor should it.</li>
</ul>
๐ My notes on publishing a Python package with UV and building a custom GitHub Action for files-to-claude-xml
https://micro.webology.dev/2024/10/16/my-notes-on-publishing-a/
Wed, 16 Oct 2024 22:07:08 -0600http://webology.micro.blog/2024/10/16/my-notes-on-publishing-a/<p>My new Python application <a href="https://pypi.org/project/files-to-claude-xml/">files-to-claude-xml</a> is now on PyPI, which means they are packaged and pip installable. My preferred way of running <code>files-to-claude-xml</code> is via UV’s tool run, which will install it if it still needs to be installed and then execute it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ uv tool run files-to-claude-xml --version
</code></pre></div><h2 id="publishing-on-pypi-with-uv">Publishing on PyPi with UV</h2>
<p>UV has both build and publish commands, so I took them for a spin today.</p>
<p><code>uv build</code> just worked, and a Python package was built.</p>
<p>When I tried <code>uv publish</code>, it prompted me for some auth settings for which I had to log in to <a href="https://pypi.org">PyPI</a> to create a token.</p>
<p>I added those to my local ENV variables I manage with <a href="https://direnv.net">direnv</a>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">export UV_PUBLISH_PASSWORD<span style="color:#f92672">=</span><your-PyPI-token-here>
export UV_PUBLISH_USERNAME<span style="color:#f92672">=</span>__token__
</code></pre></div><p>Once both were set and registered, <code>uv publish</code> published my files on PyPI.</p>
<h2 id="github-action">GitHub Action</h2>
<p>To make <code>files-to-claude-xml</code> easier to run on GitHub, I created a custom action to build a <code>_claude.xml</code> from the GitHub repository.</p>
<p>To use this action, I wrote this example workflow, which runs from <a href="https://github.com/jefftriplett/files-to-claude-xml-example">files-to-claude-xml-example</a></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#f92672">name</span>: <span style="color:#ae81ff">Convert Files to Claude XML</span>
<span style="color:#f92672">on</span>:
<span style="color:#ae81ff">push</span>
<span style="color:#f92672">jobs</span>:
<span style="color:#f92672">convert-to-xml</span>:
<span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
<span style="color:#f92672">steps</span>:
- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span>
- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Convert files to Claude XML</span>
<span style="color:#f92672">uses</span>: <span style="color:#ae81ff">jefftriplett/files-to-claude-xml-action@main</span>
<span style="color:#f92672">with</span>:
<span style="color:#f92672">files</span>: |<span style="color:#e6db74">
</span><span style="color:#e6db74"> README.md
</span><span style="color:#e6db74"> main.py</span>
<span style="color:#f92672">output</span>: <span style="color:#e6db74">'_claude.xml'</span>
<span style="color:#f92672">verbose</span>: <span style="color:#e6db74">'true'</span>
- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Upload XML artifact</span>
<span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/upload-artifact@v4</span>
<span style="color:#f92672">with</span>:
<span style="color:#f92672">name</span>: <span style="color:#ae81ff">claude-xml</span>
<span style="color:#f92672">path</span>: <span style="color:#ae81ff">_claude.xml</span>
</code></pre></div><p>My GitHub action is built with a <code>Dockerfile</code>, which installs <code>files-to-claude-xml</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-Dockerfile" data-lang="Dockerfile"><span style="color:#75715e"># Dockerfile</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> ghcr.io/astral-sh/uv:bookworm-slim</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> UV_LINK_MODE<span style="color:#f92672">=</span>copy
<span style="color:#66d9ef">RUN</span> --mount<span style="color:#f92672">=</span>type<span style="color:#f92672">=</span>cache,target<span style="color:#f92672">=</span>/root/.cache/uv <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span> --mount<span style="color:#f92672">=</span>type<span style="color:#f92672">=</span>bind,source<span style="color:#f92672">=</span>uv.lock,target<span style="color:#f92672">=</span>uv.lock <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span> --mount<span style="color:#f92672">=</span>type<span style="color:#f92672">=</span>bind,source<span style="color:#f92672">=</span>pyproject.toml,target<span style="color:#f92672">=</span>pyproject.toml <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span> uv sync --frozen --no-install-project<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">WORKDIR</span><span style="color:#e6db74"> /app</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENTRYPOINT</span> [<span style="color:#e6db74">"uvx"</span>, <span style="color:#e6db74">"files-to-claude-xml"</span>]<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>To turn a GitHub repository into a runnable GitHub Action, an <code>action.yml</code> file needs to exist in the repository. This file describes the input arguments and which <code>Dockerfile</code> or command to run.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yml" data-lang="yml"><span style="color:#75715e"># action.yml</span>
<span style="color:#f92672">name</span>: <span style="color:#e6db74">'Files to Claude XML'</span>
<span style="color:#f92672">description</span>: <span style="color:#e6db74">'Convert files to XML format for Claude'</span>
<span style="color:#f92672">inputs</span>:
<span style="color:#f92672">files</span>:
<span style="color:#f92672">description</span>: <span style="color:#e6db74">'Input files to process'</span>
<span style="color:#f92672">required</span>: <span style="color:#66d9ef">true</span>
<span style="color:#f92672">type</span>: <span style="color:#ae81ff">list</span>
<span style="color:#f92672">output</span>:
<span style="color:#f92672">description</span>: <span style="color:#e6db74">'Output XML file path'</span>
<span style="color:#f92672">required</span>: <span style="color:#66d9ef">false</span>
<span style="color:#f92672">default</span>: <span style="color:#e6db74">'_claude.xml'</span>
<span style="color:#f92672">verbose</span>:
<span style="color:#f92672">description</span>: <span style="color:#e6db74">'Enable verbose output'</span>
<span style="color:#f92672">required</span>: <span style="color:#66d9ef">false</span>
<span style="color:#f92672">default</span>: <span style="color:#e6db74">'false'</span>
<span style="color:#f92672">version</span>:
<span style="color:#f92672">description</span>: <span style="color:#e6db74">'Display the version number'</span>
<span style="color:#f92672">required</span>: <span style="color:#66d9ef">false</span>
<span style="color:#f92672">default</span>: <span style="color:#e6db74">'false'</span>
<span style="color:#f92672">runs</span>:
<span style="color:#f92672">using</span>: <span style="color:#e6db74">'docker'</span>
<span style="color:#f92672">image</span>: <span style="color:#e6db74">'Dockerfile'</span>
<span style="color:#f92672">args</span>:
- <span style="color:#ae81ff">${{ join(inputs.files, ' ') }}</span>
- --<span style="color:#ae81ff">output</span>
- <span style="color:#ae81ff">${{ inputs.output }}</span>
- <span style="color:#ae81ff">${{ inputs.verbose == 'true' && '--verbose' || '' }}</span>
- <span style="color:#ae81ff">${{ inputs.version == 'true' && '--version' || '' }}</span>
</code></pre></div><p>Overall, this works. Claude’s prompting helped me figure it out, which felt fairly satisfying given the goal of <code>files-to-claude-xml</code>.</p>
๐ค I released files-to-claude-xml and new development workflows
https://micro.webology.dev/2024/10/12/i-released-filestoclaudexml-and-new/
Sat, 12 Oct 2024 13:00:31 -0600http://webology.micro.blog/2024/10/12/i-released-filestoclaudexml-and-new/<p>After months of using and sharing this tool via a private gist, I finally carved out some time to release <a href="https://github.com/jefftriplett/files-to-claude-xml">files-to-claude-xml</a>.</p>
<p>Despite my social media timeline declaring LLMs dead earlier today, I have used <a href="https://www.anthropic.com/news/projects">Claude Projects</a> and Artifacts.</p>
<p>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.</p>
<p>My <code>files-to-claude-xml</code> script grew out of some research I did where I stumbled on their <a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/long-context-tips#essential-tips-for-long-context-prompts">Essential tips for long context prompts</a> which documents how to get around some file upload limits which encourages uploading one big file using Claude’s XML-like format.</p>
<p>With <code>files-to-claude-xml</code>, I build a list of files that I want to import into a Claude Project. Then, I run it to generate a <code>_claude.xml</code> 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.</p>
<p>After the feature is complete, I delete the <code>_claude.xml</code> file from my project and replace it with an updated copy after I re-run <code>files-to-claude-xml</code>.</p>
<h2 id="features-on-the-go">Features on the go</h2>
<p>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.</p>
<h2 id="new-workflows">New workflows</h2>
<p>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.</p>
<p>I can also get more done with my lunch hours on projects like <a href="https://djangotv.com">DjangoTV</a> 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.</p>
<p>It’s also quicker to try out new ideas and projects that I would have needed to make time for.</p>
<h2 id="alternatives">Alternatives</h2>
<p>Simon Willison wrote <a href="https://github.com/simonw/files-to-prompt">files-to-prompt</a>, which I think is also worth trying. I contributed to the discussion, feedback, and document structure for the <code>--cxml</code> feature.</p>
<p>I wrote <code>files-to-claude-xml</code> before Simon had cxml support and hoped to not release my version.</p>
<p>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.</p>
โ๏ธ UV with GitHub Actions to run an RSS to README project
https://micro.webology.dev/2024/10/05/uv-with-github-actions-to/
Sat, 05 Oct 2024 11:43:54 -0600http://webology.micro.blog/2024/10/05/uv-with-github-actions-to/<p>For my personal <a href="https://github.com/jefftriplett/jefftriplett">GitHub profile</a>, I list my activities, affiliations, and the latest updates from some of my projects.</p>
<p>Historically, I have used <a href="https://github.com/JasonEtco/rss-to-readme">JasonEtco/rss-to-readme</a> GitHub Action to fetch a few RSS feeds or two and to update my README a few times a day.</p>
<p>Overall, I’m happy with this setup. I used it on the Django News GitHub Organization to pull in newsletter issues, jobs, and the latest videos from our various projects. When I tried to install rss-to-readme in our repo, I was getting node12 errors. (Have I mentioned how much I loathe node/npm?).</p>
<p>Instead of forking rss-to-readme and trying to figure out how to upgrade it, I used this as an excuse to “pair program” with <a href="https://claude.ai">Claude</a>. We quickly built out a prototype using Python and the <a href="https://github.com/kurtmckee/feedparser">feedparser</a> library.</p>
<p>I would share the chat log, but it’s mostly me trying out a few different ways to invoke it before I settle on the finished approach. See the source code over on GitHub if you are curious: <a href="https://github.com/django-news/.github/blob/main/fetch-rss.py">https://github.com/django-news/.github/blob/main/fetch-rss.py</a></p>
<p>Once I had a working Python script that could fetch an RSS file and modify the README, I decided to run/deploy it using <a href="https://github.com/astral-sh/uv">UV</a> to see how minimal I could build out the GitHub Action.</p>
<h2 id="github-action">GitHub Action</h2>
<p>To run our <code>fetch-rss.py</code> script, we have four steps:</p>
<ol>
<li><code>actions/checkout</code> Get a git checkout of our project.</li>
<li><code>astral-sh/setup-uv</code> Setup UV also installs Pythons for us. As a bonus, we enabled UV’s cache support, which will run much faster in the future unless we change something in our fetch-rss.py file.</li>
<li>Run <code>uv run fetch-rss.py ...</code> to fetch our RSS feeds and write them to disk. <code>uv run</code> installs any dependencies and caches them before our <code>fetch-rss.py</code> runs.</li>
<li><code>stefanzweifel/git-auto-commit-action</code> If our README.md file has changed, save our changes and commit them back to git and into our README.</li>
</ol>
<p>Our <code>schedule.yml</code> GitHub Action workflow runs twice daily or whenever we push a new change to our repo. We also set <code>workflow_dispatch,</code> which gives us a button to run the script manually.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># .github/workflows/schedule.yml</span>
<span style="color:#f92672">name</span>: <span style="color:#ae81ff">Update README</span>
<span style="color:#f92672">on</span>:
<span style="color:#f92672">push</span>:
<span style="color:#f92672">branches</span>:
- <span style="color:#ae81ff">main</span>
<span style="color:#f92672">schedule</span>:
<span style="color:#75715e"># Once a day at 12 AM</span>
- <span style="color:#f92672">cron</span>: <span style="color:#ae81ff">0</span> <span style="color:#ae81ff">12</span> * * *
<span style="color:#f92672">workflow_dispatch</span>:
<span style="color:#f92672">jobs</span>:
<span style="color:#f92672">update</span>:
<span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
<span style="color:#f92672">permissions</span>:
<span style="color:#f92672">contents</span>: <span style="color:#ae81ff">write</span>
<span style="color:#f92672">steps</span>:
- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v4</span>
- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Install uv</span>
<span style="color:#f92672">uses</span>: <span style="color:#ae81ff">astral-sh/setup-uv@v3</span>
<span style="color:#f92672">with</span>:
<span style="color:#f92672">enable-cache</span>: <span style="color:#66d9ef">true</span>
<span style="color:#f92672">cache-dependency-glob</span>: |<span style="color:#e6db74">
</span><span style="color:#e6db74"> </span> <span style="color:#75715e">*.py</span>
- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Fetch our Feeds</span>
<span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span><span style="color:#e6db74"> # Fetch latest Django News Newsletter entries
</span><span style="color:#e6db74"> uv run fetch-rss.py \
</span><span style="color:#e6db74"> --section=news \
</span><span style="color:#e6db74"> --readme-path=profile/README.md \
</span><span style="color:#e6db74"> https://django-news.com/issues.rss</span>
- <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">stefanzweifel/git-auto-commit-action@v5</span>
<span style="color:#f92672">with</span>:
<span style="color:#f92672">commit_message</span>: <span style="color:#e6db74">":pencil: Updates README"</span>
</code></pre></div><h2 id="results">Results</h2>
<p>Overall, I’m pleased with this solution. If I wanted to spend more time on it or re-use this workflow, I might turn it into a GitHub Action workflow so that we can call: <code>django-news/rss-to-readme</code> to use in other projects. For now, this is fine.</p>
<p>I’m happy with the <code>astral-sh/setup-uv</code> and <code>uv run</code> steps because they save me from having to set up Python and then install our project dependencies as separate steps.</p>
<p>I normally shy away from running Python workflows like this in GitHub Actions because they involve a lot of slow steps. This entire workflow takes 16 to 20 seconds to run, which feels fast to me.</p>
๐ Announcing DjangoTV
https://micro.webology.dev/2024/09/27/announcing-djangotv/
Fri, 27 Sep 2024 16:04:31 -0600http://webology.micro.blog/2024/09/27/announcing-djangotv/<p>Friends, I’m launching a half-finished website idea that I have been playing around with for a while. <a href="https://djangotv.com">djangotv.com</a> is focused on promoting and searching Django videos to make discovery easier.</p>
<p>I wanted to launch DjangoTV before I left DjangoCon US 2024, so I’m technically announcing it from the airport. Last year, I launched <a href="https://jobs.django-news.com">Django News Jobs</a> during the Django sprints because I was annoyed by the state of the various Django job boards.</p>
<p>After a year of hearing people complain about Django and Python YouTube videos not getting enough views, I decided to address the problem by building out a website to help organize and promote them.</p>
<p>DjangoTV is not competing with pyvideo.org. PyVideo is a fantastic resource for the Python community, and one of my goals is to make it easier to get Django content on PyVideo, too.</p>
<p>DjangoTV is incomplete, and I have many ideas to improve it, including backfilling some of our older conferences, adding presenters, and adding some other metadata to make it a useful community resource.</p>
<p>I focused on DjangoCon videos to bootstrap the project, and we’ll slowly expand our archives. I have been extracting good quotes, adding transcriptions, and adding better summaries. Please expect this to change a lot. I even launched the website with DEBUG turned on for a few days before I turned it off.</p>
<p>Your feedback is crucial to us. If you have any ideas or suggestions, please don’t hesitate to share them.</p>
<p>Most importantly, names change, people change, and bodies change. It’s essential to me that we respect each other and handle this with care. If you have a concern or need us to pull a video, please don’t hesitate to reach out; we will take care of it.</p>
๐งณ DjangoCon US, Black Python Devs Leadership Summit, and Django Girls Durham
https://micro.webology.dev/2024/09/20/djangocon-us-black-python-devs/
Fri, 20 Sep 2024 06:30:00 -0600http://webology.micro.blog/2024/09/20/djangocon-us-black-python-devs/<p>I’m heading to Durham, NC, for seven days of <a href="https://2024.djangocon.us">DjangoCon US</a> this Friday. This is my 10th year volunteering and the 9th year that <a href="https://www.defna.org">DEFNA</a>, the non-profit I co-founded, has run a DjangoCon US event. Here is an overview of the week.</p>
<h2 id="black-python-devs-leadership-summit-saturday">Black Python Devs Leadership Summit (Saturday)</h2>
<p>I’m attending and speaking on a discussion panel on Saturday at the <a href="https://blackpythondevs.com/leadership-summit-2024/">Black Python Devs Leadership Summit</a>. Tickets are free, and they will be streaming online in the afternoon. Donations are accepted and appreciated.</p>
<h2 id="django-girls-durham-saturday">Django Girls Durham (Saturday)</h2>
<p><a href="https://djangogirls.org/en/durham/">Django Girls</a> are hosting a Django workshop and teaching beginners a crash course on building their first website using Django.</p>
<h2 id="djangocon-us-tutorials-sunday">DjangoCon US Tutorials (Sunday)</h2>
<p>On Sunday morning, I’ll be volunteering and helping out at the tutorials. In the afternoon, we have a tradition of stuffing swag bags, which takes a big group and is a fun way to kick off the conference. You do not need a tutorial ticket or an organizer to help out. Ask at the registration desk, and they can direct you to when and where we are doing this.</p>
<h2 id="django-social-meetup-sunday">Django Social meetup (Sunday)</h2>
<p>My company <a href="https://www.revsys.com">REVSYS</a> is sponsoring a <a href="https://www.meetup.com/djangosocial-raleighdurham/events/303526332//attendees/">DjangoSocial Raleigh/Durham Pre-DjangoCon Special</a> meetup on Sunday evening before the conference kicks off. The meetup will be great for meeting other attendees the night before the conference.</p>
<h2 id="djangocon-us-talks-monday-through-wednesday">DjangoCon US Talks (Monday through Wednesday)</h2>
<p>The talks are great, but the busiest three days of the conference are also the busiest. There is always a lot going on, from sun up to sun down.</p>
<h2 id="djangocon-us-sprints-thursday-and-friday">DjangoCon US Sprints (Thursday and Friday)</h2>
<p>The sprints are one of my favorite parts of the conference. In past years, I have been so exhausted by the sprints that it’s hard to sit down and focus. It’s one of the best times to discuss Django and the Django ecosystem. If you have a project or want to find a project to help with, the sprints are great for getting your feet wet.</p>
<h2 id="outro">Outro</h2>
<p>Tickets are still available if you live near Durham and want to attend. Both events have online and in-person options, so there is no pressure to make last-minute travel plans.</p>
<p>If you live around Durham and want to meet up, please reach out. Let’s see if we can meet for coffee.</p>
๐ค UV Roundup: Five good articles and a pre-commit tip
https://micro.webology.dev/2024/09/19/uv-roundup-five-good-articles/
Thu, 19 Sep 2024 13:18:30 -0600http://webology.micro.blog/2024/09/19/uv-roundup-five-good-articles/<p>I have written quite a bit about <a href="https://micro.webology.dev/categories/uv/">UV</a> on my <a href="https://micro.webology.dev">micro blog</a>, and I am happy to see more and more people adopt it. I have stumbled on so many good articles recently that I wanted to share them because every article points out something new or different about why UV works well for them.</p>
<p>If you are new to <a href="https://github.com/astral-sh/uv">UV</a>, it’s a new tool written by <a href="https://astral.sh/">Astral</a>, the creators of <a href="https://github.com/astral-sh/ruff">Ruff</a>.</p>
<p>I like UV because it replaces, combines, or complements a bunch of Python tools into one tool and user developer experience without forcing a UV way of doing it. UV effectively solves the question, “Why do I need another Python tool?” to do everyday Python tasks.</p>
<p>Some reason I like UV after using it for months:</p>
<ul>
<li>It’s a faster pip and is really, really fast</li>
<li>It can install and manage Python versions</li>
<li>It can run and install Python scripts</li>
<li>It can run single-file Python scripts along with their dependencies</li>
<li>It can handle project lock files</li>
</ul>
<p>While some people don’t care about UV being fast, it’s shaved minutes off my CI builds and container rebuilds, which means it has also saved me money and energy resources.</p>
<h2 id="overall-thoughts-on-uv">Overall thoughts on UV</h2>
<p>Oliver Andrich’s <a href="https://andrich.me/2024/09/uv-i-am-somewhat-sold/">UV โ I am (somewhat) sold</a> takes the approach of only using UV to set up a new Python environment. Oliver uses UV to install Python, aliases to call Python, and UV tool install to set up a few global utilities.</p>
<h2 id="using-uv-with-django">Using UV with Django</h2>
<p>Anลพe Peฤar’s <a href="https://blog.pecar.me/uv-with-django">UV with Django</a> shows how to use UV to set up a new project with Django.</p>
<h2 id="switching-from-pyenv-to-uv">Switching from pyenv to UV</h2>
<p>Will Guaraldi Kahn-Greene’s <a href="https://bluesock.org/~willkg/blog/dev/switch_pyenv_to_uv.html">Switching from pyenv to uv</a> was relatable for me because I also use pyenv, but I plan to slowly migrate to using only UV. I’m already halfway there, but I will have pyenv for my legacy projects for years because many aren’t worth porting yet.</p>
<h2 id="using-uv-and-managing-with-ansible">Using UV and managing with Ansible</h2>
<p>Adam Johnson’s <a href="https://adamj.eu/tech/2024/09/18/python-uv-development-setup/">Python: my new uv setup for development</a> taught me to use <code>uv cache prune</code> to clean up unused cache entries and shows how he manages his UV setup using Ansible.</p>
<h2 id="some-notes-on-uv">Some notes on UV</h2>
<p>Simon Willison’s <a href="https://simonwillison.net/2024/Sep/15/uv-i-am-somewhat-sold/">Notes on UV</a> is an excellent summary of Oliver’s notes.</p>
<h2 id="a-parting-uv-tip">A parting UV tip</h2>
<p>If you are a pre-commit fan hoping for a version that supports UV, the <a href="https://github.com/tox-dev/pre-commit-uv"><code>pre-commit-uv</code></a> project does just that. I started updating my justfile recipes to bake <code>just lint</code> to the following <code>uv run</code> command, which speeds up running and installing pre-commit significantly.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ uv run --with pre-commit-uv pre-commit run --all-files
pre-commit-uv
</code></pre></div><h2 id="if-you-are-attending-djangocon-us">If you are attending DjangoCon US…</h2>
<p>If you are attending DjangoCon US and want to talk UV, Django, <a href="https://django-news.com">Django News</a>, <a href="https://djangopackages.org">Django Packages</a>, hit me up while you are there.</p>
<p>I’ll be attending, volunteering, organizing, <a href="https://mastodon.social/@[email protected]/113165161818979000">sponsoring</a>, and sprinting around the venue in Durham, NC, for the next week starting this Friday.</p>
<p>We still have <a href="https://ti.to/defna/djangocon-us-2024">online and in-person tickets</a>, but not much longer!</p>
๐ Office Hours Fall Update
https://micro.webology.dev/2024/09/12/office-hours-fall-update/
Thu, 12 Sep 2024 06:00:00 -0600http://webology.micro.blog/2024/09/12/office-hours-fall-update/<p>This Friday, I am hosting Office Hours before I travel to <a href="https://2024.djangocon.us">DjangoCon US</a> (organizer) and the <a href="https://blackpythondevs.com/leadership-summit-2024/">Black Python Devs Leadership Summit</a> (speaker) in Durham, NC.</p>
<p>This Friday will be my last session before a two-week break, but I will resume Office Hours again on October 4th.</p>
<ul>
<li>Friday, September 13th, 2024, <a href="https://time.is/0230PM_13_September_2024_in_CT?Jeff%27s_Office_Hours">2:30 pm to 4:30 pm</a></li>
<li>Friday, September 20th, 2024 - No office hours</li>
<li>Friday, September 27th, 2024 - No office hours</li>
<li>Friday, October 4th, 2024, <a href="https://time.is/0230PM_04_October_2024_in_CT?Jeff%27s_Office_Hours">2:30 pm to 4:30 pm</a></li>
</ul>
<h2 id="high-level-details">High-level details</h2>
<p>โน๏ธ 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.</p>
<p>โน๏ธ 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.</p>
<p>๐ 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.</p>
<p>โ 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 โ ๏ธ</p>
๐ซ Stop scheduling security updates and deprecating major features over holidays
https://micro.webology.dev/2024/09/03/stop-scheduling-security-updates-and/
Tue, 03 Sep 2024 08:24:54 -0600http://webology.micro.blog/2024/09/03/stop-scheduling-security-updates-and/<p>I know people outside the US ๐ at this, but please stop releasing major security updates and backward incompatible changes over major US,ย international, and religious holidays.</p>
<p>Given that major security updates are embargoed and scheduled weeks and months in advance, it’s essential to coordinate and avoid conflicts. A simple check of the calendar before scheduling announcements can prevent such issues.</p>
<p>Even if you give everyone two weeks' notice, aka what GitHub just did, wait to schedule them for release over a holiday weekend.</p>
<p>Historically, the Python and Django communities have also been guilty of this, so I’m not just finger-pointing at GitHub. We can all do better here.</p>
<p><strong>Update:</strong> 100% unrelated to this: <a href="https://www.djangoproject.com/weblog/2024/sep/03/security-releases/">Django security releases issued: 5.1.1, 5.0.9, and 4.2.16</a>. Thank you, Natalia (and Sarah) for scheduling this <em>after</em> the US is back from a major holiday.</p>
๐ Using Claude 3.5 Sonnet to refactor one of Brian Okken's Python projects
https://micro.webology.dev/2024/08/25/using-claude-sonnet-to-refactor/
Sun, 25 Aug 2024 13:46:23 -0600http://webology.micro.blog/2024/08/25/using-claude-sonnet-to-refactor/<p>Brian Okken <a href="https://mastodon.social/@[email protected]/113023568032444293">posted</a> and published his <a href="https://pythontest.com/top-pytest-plugins/">Top pytest Plugins</a> script and then a follow-up post, <a href="https://pythontest.com/pytest/finding-top-pytest-plugins/">Finding the top pytest plugins</a>, which was pretty cool.</p>
<p>I have written a few throw-away scripts, which William Vincent wrote about and updated a few times in the <a href="https://learndjango.com/tutorials/essential-django-3rd-party-packages">Top 10 Django Third-Party Packages (2024)</a> and <a href="https://learndjango.com/tutorials/10-most-used-django-packages">The 10 Most-Used Django Packages (2024)</a>.</p>
<p>These efforts are powered by <a href="https://github.com/hugovk/">Hugo van Kemenade</a>’s excellent <a href="https://hugovk.github.io/top-pypi-packages/">Top PyPI Packages</a>.</p>
<p>This inspired me to fork Brian’s <a href="https://github.com/okken/top-pytest-plugins">top-pytest-plugins</a> project, which I updated to support passing in other package names like “django” to get a rough estimate of monthly package downloads.</p>
<p>The refactored project is <a href="https://github.com/jefftriplett/top-python-packages">jefftriplett/top-python-packages</a>.</p>
<p><strong>Please note:</strong> Looking at the package name doesn’t scale as well for projects that have their own <a href="https://pypi.org/classifiers/">Trove classifiers</a>. For a project like pytest, it works well. Many of the top packages may not even have Django in their name for a project like Django. Some projects may even actively discourage a project from using their project in their package’s name for trademark reasons. So, YMMV applies here.</p>
<h2 id="prompts">Prompts</h2>
<p>I added <code>uv run</code> support, which I have <a href="https://micro.webology.dev/categories/uv/">written about a lot lately</a>.</p>
<p>I also copied the <code>top_pytest.py</code> file into a Claude 3.5 Sonnet session, and I let it handle the whole refactor. It even handled adding the <a href="https://peps.python.org/pep-0723/">PEP 723</a> new package dependencies without me asking it to.</p>
<p>In case it’s useful to anyone, here are my prompts:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-plaintext" data-lang="plaintext">## Prompt:
Please update this script to use a rich table.
## Prompt:
Please update the table styles to be ascii so I can copy and paste it into a markdown doc
## Prompt:
Please remove the description column
## Prompt:
Please change all PyTest and pytest references to Django and django
## Prompt:
Please add back `if 'django' in project.lower() and 'django' != project.lower():`
## Prompt:
please remove the \*# Export to markdown section. I can just pipe the output \*
## Prompt:
Please add the typer library.
## Prompt:
Please remove days and limit
## Prompt:
Please refactor the script to allow me to pass the package name instead of django. You can default to django though.
This way I can pass pytest or flask or other projects.
## Prompt:
Please change the default Table box type to MARKDOWN
</code></pre></div><h2 id="outro">Outro</h2>
<p>I don’t usually write about Claude or prompts, but the tool has been handy lately.</p>
<p>If you have had some similar successes, let me know. I have been exploring some rabbit holes, and it’s changing the way I approach solving problems.</p>
๐ UV Run Django Notes
https://micro.webology.dev/2024/08/23/uv-run-django-notes/
Fri, 23 Aug 2024 06:45:00 -0600http://webology.micro.blog/2024/08/23/uv-run-django-notes/<p>I wanted to know how hard it would be to turn one of my <a href="https://github.com/jefftriplett/django-startproject">django-startproject</a> projects into a <code>uv run</code> friendly project. As it turns out, it worked, and the steps were more than reasonable.</p>
<h2 id="before-the-pep-723ing">Before the PEP 723’ing…</h2>
<p>I started with a fairly vanilla <code>manage.py</code> that Django will give you after running <code>python -m manage startproject</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#e6db74">"""Django's command-line utility for administrative tasks."""</span>
<span style="color:#f92672">import</span> os
<span style="color:#f92672">import</span> sys
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
<span style="color:#e6db74">"""Run administrative tasks."""</span>
os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>setdefault(<span style="color:#e6db74">"DJANGO_SETTINGS_MODULE"</span>, <span style="color:#e6db74">"config.settings"</span>)
<span style="color:#66d9ef">try</span>:
<span style="color:#f92672">from</span> django.core.management <span style="color:#f92672">import</span> execute_from_command_line
<span style="color:#66d9ef">except</span> <span style="color:#a6e22e">ImportError</span> <span style="color:#66d9ef">as</span> exc:
<span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">ImportError</span>(
<span style="color:#e6db74">"Couldn't import Django. Are you sure it's installed and "</span>
<span style="color:#e6db74">"available on your PYTHONPATH environment variable? Did you "</span>
<span style="color:#e6db74">"forget to activate a virtual environment?"</span>
) <span style="color:#f92672">from</span> exc
execute_from_command_line(sys<span style="color:#f92672">.</span>argv)
<span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">"__main__"</span>:
main()
</code></pre></div><h2 id="shebang">shebang</h2>
<p>Then we add <code>#!/usr/bin/env -S uv run</code> to the top of our <code>manage.py</code> file.</p>
<p>Next, we make our <code>manage.py</code> executable and try to run it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ chmod +x manage.py
$ ./manage.py
ModuleNotFoundError: No module named <span style="color:#e6db74">'django'</span>
</code></pre></div><p>Our script ran, but Python couldn’t find Django. To tell our script to install Django, we can use <code>uv addโ- script</code> to add it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ uv add --script manage.py django
Updated <span style="color:#e6db74">`</span>manage.py<span style="color:#e6db74">`</span>
$ ./manage.py
...
Type <span style="color:#e6db74">'manage.py help <subcommand>'</span> <span style="color:#66d9ef">for</span> help on a specific subcommand.
Available subcommands:
<span style="color:#f92672">[</span>django<span style="color:#f92672">]</span>
check
compilemessages
createcachetable
dbshell
diffsettings
dumpdata
flush
inspectdb
loaddata
makemessages
makemigrations
migrate
optimizemigration
runserver
sendtestemail
shell
showmigrations
sqlflush
sqlmigrate
sqlsequencereset
squashmigrations
startapp
startproject
test
testserver
Note that only Django core commands are listed as settings are not properly configured <span style="color:#f92672">(</span>error: No module named <span style="color:#e6db74">'environs'</span><span style="color:#f92672">)</span>.
</code></pre></div><p>Django worked as expected this time, but Python could not find a few third-party libraries I like to include in my projects.</p>
<p>To add these, I passed the other four to <code>uv add --script</code> which will add them to the project.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ uv add --script manage.py django-click <span style="color:#e6db74">"environs[django]"</span> psycopg2-binary whitenoise
Updated <span style="color:#e6db74">`</span>manage.py<span style="color:#e6db74">`</span>
...
$ ./manage.py
...
</code></pre></div><p>Our Django app’s <code>manage.py</code> works when we run it.</p>
<h2 id="after-the-pep-723ing">After the PEP 723’ing…</h2>
<p>After we installed our dependencies in our <code>manage.py</code> file, they were added to the top of the file between the <code>///</code> blocks.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e">#!/usr/bin/env -S uv run</span>
<span style="color:#75715e"># /// script</span>
<span style="color:#75715e"># requires-python = ">=3.10"</span>
<span style="color:#75715e"># dependencies = [</span>
<span style="color:#75715e"># "django",</span>
<span style="color:#75715e"># "django-click",</span>
<span style="color:#75715e"># "environs[django]",</span>
<span style="color:#75715e"># "psycopg2-binary",</span>
<span style="color:#75715e"># "whitenoise",</span>
<span style="color:#75715e"># ]</span>
<span style="color:#75715e"># ///</span>
<span style="color:#e6db74">"""Django's command-line utility for administrative tasks."""</span>
<span style="color:#f92672">import</span> os
<span style="color:#f92672">import</span> sys
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
<span style="color:#e6db74">"""Run administrative tasks."""</span>
os<span style="color:#f92672">.</span>environ<span style="color:#f92672">.</span>setdefault(<span style="color:#e6db74">"DJANGO_SETTINGS_MODULE"</span>, <span style="color:#e6db74">"config.settings"</span>)
<span style="color:#66d9ef">try</span>:
<span style="color:#f92672">from</span> django.core.management <span style="color:#f92672">import</span> execute_from_command_line
<span style="color:#66d9ef">except</span> <span style="color:#a6e22e">ImportError</span> <span style="color:#66d9ef">as</span> exc:
<span style="color:#66d9ef">raise</span> <span style="color:#a6e22e">ImportError</span>(
<span style="color:#e6db74">"Couldn't import Django. Are you sure it's installed and "</span>
<span style="color:#e6db74">"available on your PYTHONPATH environment variable? Did you "</span>
<span style="color:#e6db74">"forget to activate a virtual environment?"</span>
) <span style="color:#f92672">from</span> exc
execute_from_command_line(sys<span style="color:#f92672">.</span>argv)
<span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">"__main__"</span>:
main()
</code></pre></div>๐ Python UV run with shebangs
https://micro.webology.dev/2024/08/22/python-uv-run-with-shebangs/
Thu, 22 Aug 2024 12:54:45 -0600http://webology.micro.blog/2024/08/22/python-uv-run-with-shebangs/<p>This <a href="https://simonwillison.net/2024/Aug/21/usrbinenv-uv-run/">UV shebang</a> trick that Simon Willison linked up is a nice pattern, and I plan to rebuild some of my one-off scripts in my <a href="https://github.com/jefftriplett/dotfiles">dotfiles</a> using it.</p>
<p>Here is a demo that will print “hello python” using the Python Branding colors using the <a href="https://github.com/Textualize/rich">Rich</a> library while letting UV install and manage rich for you.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e">#!/usr/bin/env -S uv run</span>
<span style="color:#75715e"># /// script</span>
<span style="color:#75715e"># requires-python = ">=3.10"</span>
<span style="color:#75715e"># dependencies = [</span>
<span style="color:#75715e"># "rich",</span>
<span style="color:#75715e"># ]</span>
<span style="color:#75715e"># ///</span>
<span style="color:#f92672">from</span> rich.console <span style="color:#f92672">import</span> Console
<span style="color:#f92672">from</span> rich.theme <span style="color:#f92672">import</span> Theme
python_theme <span style="color:#f92672">=</span> Theme(
{
<span style="color:#e6db74">"pyyellow"</span>: <span style="color:#e6db74">"#ffde57"</span>,
<span style="color:#e6db74">"pyblue"</span>: <span style="color:#e6db74">"#4584b6"</span>,
}
)
console <span style="color:#f92672">=</span> Console(theme<span style="color:#f92672">=</span>python_theme)
console<span style="color:#f92672">.</span>print(<span style="color:#e6db74">"[pyyellow]hello[/pyyellow] [pyblue]python[/pyblue]"</span>, style<span style="color:#f92672">=</span><span style="color:#e6db74">"on #646464"</span>)
</code></pre></div><p>Assuming you have UV installed, and you save and <code>chmod +x</code> this file as <code>hello-python.py</code>, then you should be able to run it via <code>./hello-python.py.</code></p>
<p>I suspect I can more easily bootstrap new machines using this trick without fewer worries about polluting my global system packages.</p>
๐ UV Updates and PEP 723: Simplifying Python Packaging and Scripting
https://micro.webology.dev/2024/08/21/uv-updates-and-pep-simplifying/
Wed, 21 Aug 2024 21:57:49 -0600http://webology.micro.blog/2024/08/21/uv-updates-and-pep-simplifying/<p>The <a href="https://astral.sh/blog/uv-unified-python-packaging">uv: Unified Python packaging</a> update brings fresh air to the Python community, with several improvements streamlining the development process. One exciting addition is an early preview of <a href="https://peps.python.org/pep-0723/">PEP 723</a>, also known as <a href="https://astral.sh/blog/uv-unified-python-packaging#single-file-scripts">Single-file scripts</a>.</p>
<p>The Single-file scripts feature particularly caught my attention due to its potential to simplify the distribution and execution of small Python projects. Streamlining the process is highly appealing to someone who frequently creates GitHub Gists and shares them privately and publicly.</p>
<p>With this new feature, I can now instruct users to run <code>uv run main.py</code> without explaining what a <code>venv</code> or <code>virtualenv</code> is, plus a long list of requirements that need to be passed to <code>pip install</code>.</p>
<p>I had the opportunity to test this feature over lunch today. While adding libraries to the script was straightforward, I encountered a few hurdles when I forgot to invoke <code>uv run</code> in my virtual environment (venv). This makes sense, given that it’s a new habit, but it highlights the importance of adapting to changes in our development workflow.</p>
<p>Overall, the UV: Unified Python packaging update and the introduction of Single-file scripts mark a significant step in simplifying Python development. As developers become more familiar with these improvements, we expect increased adoption and smoother collaboration on small-scale projects.</p>
<h2 id="bonus-example">Bonus Example</h2>
<p>I looked through some of my recent visits, and one I recently shared with a few conference organizer friends was a one-off script I used to read several YouTube video JSON files that I’m using to bootstrap another project. It was the first time I used <a href="https://duckdb.org">DuckDB</a> to make quick work of reading data from a bunch of JSON files using SQL.</p>
<p>Overall, I was happy with DuckDB and what PEP 723 might bring to the future of Python apps, even if my example only does a little.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># To run this application, use:</span>
<span style="color:#75715e"># uv run demo-duckdb.py</span>
<span style="color:#75715e">#</span>
<span style="color:#75715e"># /// script</span>
<span style="color:#75715e"># requires-python = ">=3.10"</span>
<span style="color:#75715e"># dependencies = [</span>
<span style="color:#75715e"># "duckdb",</span>
<span style="color:#75715e"># "rich",</span>
<span style="color:#75715e"># "typer",</span>
<span style="color:#75715e"># ]</span>
<span style="color:#75715e"># ///</span>
<span style="color:#f92672">import</span> duckdb
<span style="color:#f92672">import</span> typer
<span style="color:#f92672">from</span> rich <span style="color:#f92672">import</span> print
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
result <span style="color:#f92672">=</span> duckdb<span style="color:#f92672">.</span>sql(<span style="color:#e6db74">"SELECT id,snippet FROM read_json('json/*.json')"</span>)<span style="color:#f92672">.</span>fetchall()
<span style="color:#66d9ef">for</span> row <span style="color:#f92672">in</span> result:
id, snippet <span style="color:#f92672">=</span> row
print(<span style="color:#e6db74">"-"</span> <span style="color:#f92672">*</span> <span style="color:#ae81ff">80</span>)
print(<span style="color:#e6db74">f</span><span style="color:#e6db74">"</span><span style="color:#e6db74">{</span>id<span style="color:#e6db74">=}</span><span style="color:#e6db74">"</span>)
print(<span style="color:#e6db74">f</span><span style="color:#e6db74">"</span><span style="color:#e6db74">{</span>snippet[<span style="color:#e6db74">'channelTitle'</span>]<span style="color:#e6db74">=}</span><span style="color:#e6db74">"</span>)
print(<span style="color:#e6db74">f</span><span style="color:#e6db74">"</span><span style="color:#e6db74">{</span>snippet[<span style="color:#e6db74">'title'</span>]<span style="color:#e6db74">=}</span><span style="color:#e6db74">"</span>)
print(<span style="color:#e6db74">f</span><span style="color:#e6db74">"</span><span style="color:#e6db74">{</span>snippet[<span style="color:#e6db74">'publishedAt'</span>]<span style="color:#e6db74">=}</span><span style="color:#e6db74">"</span>)
print(snippet[<span style="color:#e6db74">"description"</span>])
print(snippet[<span style="color:#e6db74">"thumbnails"</span>]<span style="color:#f92672">.</span>get(<span style="color:#e6db74">"maxres"</span>) <span style="color:#f92672">or</span> snippet<span style="color:#f92672">.</span>get(<span style="color:#e6db74">"standard"</span>))
print()
<span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">"__main__"</span>:
typer<span style="color:#f92672">.</span>run(main)
</code></pre></div><p>Overall, the future is bright with UV and PEP 723 may bring us. I’m excited to have more one-file Python apps that are easier to share and run with others.</p>
<p>PEP 723 also opens the door to turning a one-file Python script into a runnable Docker image that doesn’t even need Python on the machine or opens the door for <a href="https://beeware.org">Beeware</a> and <a href="https://beeware.org/project/projects/tools/briefcase/">Briefcase</a> to build standalone apps.</p>
๐ผ Office Hours this Friday, August 9th
https://micro.webology.dev/2024/08/08/office-hours-this-friday-august/
Thu, 08 Aug 2024 15:26:15 -0600http://webology.micro.blog/2024/08/08/office-hours-this-friday-august/<p>I am hosting Office Hours this <a href="https://time.is/0230PM_09_August_2024_in_CT?Jeff%27s_Office_Hours">Friday, August 9th, 2024, from 2:30 pm to 4:30 pm Central Time</a>.</p>
<p>๐ผ I plan to upgrade some projects to work with Django 5.1 and possibly Python 3.13 as time permits. I have a few Django Packages tasks to finish too.</p>
<p>โน๏ธ Anyone can join office hours.</p>
<p>โน๏ธ 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.</p>
<p>๐ As always, everyone is welcome to join, whether youโre a regular attendee or joining for the first time.</p>
<p>โ 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 find the Zoom link โ ๏ธ</p>
<p>I look forward to seeing everyone.</p>
โฌ๏ธ Which Django and Python versions should I be using today?
https://micro.webology.dev/2024/08/07/which-django-and-python-versions/
Wed, 07 Aug 2024 21:05:56 -0600http://webology.micro.blog/2024/08/07/which-django-and-python-versions/<p><a href="https://www.djangoproject.com/weblog/2024/aug/07/django-51-released/">Django 5.1 was released</a>, and I was reminded of the article I wrote earlier this year about <a href="https://micro.webology.dev/2024/02/01/choosing-the-right.html">Choosing the Right Python and Django Versions for Your Projects</a>.</p>
<p>While I encouraged you to wait until the second, third, or even fourth patch release of Django and Python before upgrading, I received a bit of pushback. One interesting perspective claimed that if everyone waits to upgrade, we don’t find critical bugs until the later versions. While that may be plausible, I don’t believe that the dozens of people who read my blog will be swayed by my recommendation to wait for a few patch releases.</p>
<p>I could have emphasized the potential risks of not testing early. Please start testing during the alpha and release candidate phase so that when Django 5.1 is released, your third-party applications will be ready and working on launch day, minimizing the risk of last-minute issues.</p>
<p>Today, I tried to upgrade <a href="https://djangopackages.org">Django Packages</a> to run on Django 5.1 to see if our test suite would run on Django 5.1, and it very quickly failed in CI due to at least one package not supporting 5.1 yet. Even if it had passed, I’m 90% sure another package would have failed because that’s the nature of running a new major Django or Python release on day one. Even if the third-party package is ready, the packaging ecosystem needs time to catch up.</p>
<h2 id="which-version-of-django-should-i-use-today">Which version of Django should I use today?</h2>
<p>I’m sticking with <strong>Django 5.0</strong> until Django 5.1’s ecosystem has caught up. I plan to update the third-party packages I help maintain to have Django 5.1 support. After a few patch releases of Django 5.1 have come out and the ecosystem has time to catch up, I will try to migrate again.</p>
<h2 id="which-version-of-python-should-i-use-today">Which version of Python should I use today?</h2>
<p>I’m starting new projects on <strong>Python 3.12</strong>, with a few legacy projects still being done on Python 3.11. While I am adding Django 5.1 support, I plan to add Python 3.13 support in my testing matrixes to prepare everything for Python 3.13’s release this fall.</p>
<h2 id="office-hours">Office hours</h2>
<p>I plan to spend some of my <a href="https://micro.webology.dev/categories/office-hours/">Office Hours</a> this week working on Django 5.1 and Python 3.13 readiness for projects I maintain. Please join me if you have a project to update or would like some light-hearted banter to end your week.</p>
๐ฆ Ollama Tool Calling Loose Notes
https://micro.webology.dev/2024/07/26/ollama-tool-calling-loose-notes/
Fri, 26 Jul 2024 22:30:18 -0600http://webology.micro.blog/2024/07/26/ollama-tool-calling-loose-notes/<p>I spent a <a href="https://micro.webology.dev/2024/07/24/ollama-llama-red.html">few hours this week</a> working with the Ollama project and trying to get tool calling to work with the LangChain library.</p>
<p>Tool calling is a way to expose Python functions to a language model that allows them to be called. This will enable models to perform more complex actions and even call the outside world for more information.</p>
<p>I haven’t used LangChain before, and I found the whole process frustrating. The docs were full of errors. I eventually figured it out, but I was limited to one tool call per prompt, which felt broken.</p>
<p>Earlier today, I was telling a colleague about it, and when we got back from grabbing coffee, I thought I would check the Ollama Discord channel to see if anyone else had figured it out. To my surprise, they added and released <a href="https://ollama.com/blog/tool-support">Tool support</a> last night, which allowed me to ditch LangChain altogether.</p>
<p>The Ollama project’s <a href="https://github.com/ollama/ollama-python/blob/main/examples/tools/main.py">tool calling example</a> was just enough to help get me started.</p>
<p>I struggled with the function calling syntax, but after digging a bit deeper, I found this example from <a href="https://platform.openai.com/docs/assistants/tools/function-calling">OpenAI’s Function calling docs</a>, which matches the format the Ollama project is following. I still don’t fully understand it, but I got more functions working and verified that I can make multiple tool calls within the same prompt.</p>
<p>Meta’s Llama 3.1 model supports tool calling, and the two work quite well together. I am also impressed with Llama 3.1 and the large context window support. I’m running the 8B and 70B models on a Mac Studio, and they feel very close to the commercial APIs I have worked with, but I can run them locally.</p>
<h2 id="embedding-models">Embedding models</h2>
<p>Tonight, I tried out Ollama’s <a href="https://ollama.com/blog/embedding-models">Embedding models</a> example, and while I got it working, I still need to put practical data into it to give it a better test</p>
<h2 id="one-more-tip">One more tip</h2>
<p>If you did not know Ollama can parse and return valid JSON, check out <a href="https://whoa.fyi/how-to-get-json-response-from-ollama/">How to get JSON response from Ollama</a>. It made my JSON parsing and responses much more reliable.</p>
๐ฆ Ollama Llama 3.1 Red Pajama
https://micro.webology.dev/2024/07/24/ollama-llama-red-pajama/
Wed, 24 Jul 2024 12:02:09 -0600http://webology.micro.blog/2024/07/24/ollama-llama-red-pajama/<p>For a few weeks, I told friends I was excited to see if the new Llama 3.1 release was as good as it was being hyped.</p>
<p>Yesterday, Llama 3.1 was released, and I was impressed that the <a href="https://ollama.com">Ollama</a> project published a release to <a href="https://brew.sh">Homebrew</a> and had the models ready to use.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">โ brew install ollama
โ ollama serve
<span style="color:#75715e"># (optionally) I run Ollama as a background service</span>
โ brew services start ollama
<span style="color:#75715e"># This takes a while (defaults to the llama3.1:8b model)</span>
โ ollama pull llama3.1:latest
<span style="color:#75715e"># (optional) This takes a longer time</span>
โ ollama pull llama3.1:70b
<span style="color:#75715e"># (optional) This takes so long that I skipped it and ordered a CAT6 cable...</span>
<span style="color:#75715e"># ollama pull llama3.1:405b</span>
</code></pre></div><p>To use chat with the model, you use the same <code>ollama</code> console command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">โ ollama run llama3.1:latest
>>> how much is 2+2?
The answer to <span style="color:#ae81ff">2</span> + <span style="color:#ae81ff">2</span> is:
4!<span style="color:#e6db74">```</span>
<span style="color:#75715e">## Accessing Ollama Llama 3.1 with Python</span>
The Ollama project has an <span style="color:#f92672">[</span><span style="color:#e6db74">`</span>ollama-python<span style="color:#e6db74">`</span><span style="color:#f92672">](</span>https://github.com/ollama/ollama-python<span style="color:#f92672">)</span> library, which I use to build applications.
My demo has a bit of flare because there are a few options, like <span style="color:#e6db74">`</span>--stream,<span style="color:#e6db74">`</span> that improve the quality of life <span style="color:#66d9ef">while</span> waiting <span style="color:#66d9ef">for</span> Ollama to <span style="color:#66d9ef">return</span> results.
<span style="color:#e6db74">```</span>python
<span style="color:#75715e"># hello-llama.py</span>
import typer
from enum import Enum
from ollama import Client
from rich import print
class Host<span style="color:#f92672">(</span>str, Enum<span style="color:#f92672">)</span>:
local <span style="color:#f92672">=</span> <span style="color:#e6db74">"http://127.0.0.1:11434"</span>
the_office <span style="color:#f92672">=</span> <span style="color:#e6db74">"http://the-office:11434"</span>
class ModelChoices<span style="color:#f92672">(</span>str, Enum<span style="color:#f92672">)</span>:
llama31 <span style="color:#f92672">=</span> <span style="color:#e6db74">"llama3.1:latest"</span>
llama31_70b <span style="color:#f92672">=</span> <span style="color:#e6db74">"llama3.1:70b"</span>
def main<span style="color:#f92672">(</span>
host: Host <span style="color:#f92672">=</span> Host.local,
local: bool <span style="color:#f92672">=</span> False,
model: ModelChoices <span style="color:#f92672">=</span> ModelChoices.llama31,
stream: bool <span style="color:#f92672">=</span> False,
<span style="color:#f92672">)</span>:
<span style="color:#66d9ef">if</span> local:
host <span style="color:#f92672">=</span> Host.local
client <span style="color:#f92672">=</span> Client<span style="color:#f92672">(</span>host<span style="color:#f92672">=</span>host.value<span style="color:#f92672">)</span>
response <span style="color:#f92672">=</span> client.chat<span style="color:#f92672">(</span>
model<span style="color:#f92672">=</span>model.value,
messages<span style="color:#f92672">=[</span>
<span style="color:#f92672">{</span>
<span style="color:#e6db74">"role"</span>: <span style="color:#e6db74">"user"</span>,
<span style="color:#e6db74">"content"</span>: <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span> <span style="color:#e6db74">"Please riff on the 'Llama Llama Red Pajama' book but using AI terms like the 'Ollama' server and the 'Llama 3.1' model."</span>
<span style="color:#e6db74">"Instead of using 'Llama Llama', please use 'Ollama Llama 3.1'."</span>,
<span style="color:#f92672">}</span>
<span style="color:#f92672">]</span>,
stream<span style="color:#f92672">=</span>stream,
<span style="color:#f92672">)</span>
<span style="color:#66d9ef">if</span> stream:
<span style="color:#66d9ef">for</span> chunk in response:
print<span style="color:#f92672">(</span>chunk<span style="color:#f92672">[</span><span style="color:#e6db74">"message"</span><span style="color:#f92672">][</span><span style="color:#e6db74">"content"</span><span style="color:#f92672">]</span>, end<span style="color:#f92672">=</span><span style="color:#e6db74">""</span>, flush<span style="color:#f92672">=</span>True<span style="color:#f92672">)</span>
print<span style="color:#f92672">()</span>
<span style="color:#66d9ef">else</span>:
print<span style="color:#f92672">(</span>f<span style="color:#e6db74">"[yellow]{response['message']['content']}[/yellow]"</span><span style="color:#f92672">)</span>
<span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">"__main__"</span>:
typer.run<span style="color:#f92672">(</span>main<span style="color:#f92672">)</span>
</code></pre></div><p>Some of my family’s favorite books are the late Anna Dewdney’s <a href="https://llamallamabook.com">Llama Llama books</a>. Please buy and support their work. I can’t read Llama 3.1 and Ollama without considering the “Llama Llama Red Pajama” book.</p>
<p>To set up and run this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#75715e"># Install a few "nice to have" libraries</span>
โ pip install ollama rich typer
<span style="color:#75715e"># Run our demo</span>
โ python hello-llama.py --stream
Here<span style="color:#e6db74">'s a riff on "Llama Llama Red Pajama" but with an AI twist:
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">**Ollama Llama 3.1, Ollama Llama 3.1**
</span><span style="color:#e6db74">Mama said to Ollama Llama 3.1,
</span><span style="color:#e6db74">"Dinner'</span>s <span style="color:#66d9ef">done</span>, time <span style="color:#66d9ef">for</span> some learning fun!<span style="color:#e6db74">"
</span><span style="color:#e6db74">But Ollama Llama 3.1 didn't wanna play
</span><span style="color:#e6db74">With the data sets and algorithms all day.
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">He wanted to go out and get some rest,
</span><span style="color:#e6db74">And dream of neural nets that were truly blessed.
</span><span style="color:#e6db74">But Mama said, "</span>No way, young Ollama Llama 3.1,
You need to train on some more NLP.<span style="color:#e6db74">"
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">Ollama Llama 3.1 got so mad and blue
</span><span style="color:#e6db74">He shouted at the cloud, "</span>I don<span style="color:#e6db74">'t wanna do this too!"
</span><span style="color:#e6db74">But then he remembered all the things he could see,
</span><span style="color:#e6db74">On the Ollama server, where his models would be.
</span><span style="color:#e6db74">
</span><span style="color:#e6db74">So he plugged in his GPU and gave a happy sigh
</span><span style="color:#e6db74">And trained on some texts, till the morning light shone high.
</span><span style="color:#e6db74">He learned about embeddings and wordplay too,
</span><span style="color:#e6db74">And how to chat with humans, that'</span>s what he wanted to <span style="color:#66d9ef">do</span>.
**The end**
</code></pre></div><h2 id="connecting-to-ollama">Connecting to Ollama</h2>
<p>I have two Macs running Ollama and I use <a href="https://tailscale.com">Tailscale</a> to bounce between them from anywhere. When I’m at home upstairs it’s quicker to run a local instance. When I’m on my 2019 MacBook Pro it’s faster to connect to the office.</p>
<p>The only stumbling block I ran into was needing to set a few ENV variables setup so that Ollama is listening on a port that I can proxy to. This was frustrating to figure out, but I hope it saves you some time.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">โ launchctl setenv OLLAMA_HOST 0.0.0.0:11434
โ launchctl setenv OLLAMA_ORIGINS http://*
<span style="color:#75715e"># Restart the Ollama server to pick up on the ENV vars</span>
โ brew services restart ollama
</code></pre></div><h2 id="simon-willisons-llm-tool">Simon Willison’s LLM tool</h2>
<p>I also like using <a href="https://simonwillison.net">Simon Willison</a>’s <a href="https://llm.datasette.io/en/stable/">LLM</a> tool, which supports a ton of different AI services via third-party plugins. I like the <a href="https://pypi.org/project/llm-ollama/">llm-ollama</a> library, which allows us to connect to our local Ollama instance.</p>
<p>When working with Ollama, I start with the Ollama run command, but I have a few bash scripts that might talk to OpenAI or Claude 3.5, and it’s nice to keep my brain in the same tooling space. LLM is useful for mixing and matching remote and local models.</p>
<p>To install and use LLM + llm-ollama + Llama 3.1.</p>
<p>Please note that the Ollama server should already be running as previously outlined.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#75715e"># Install llm</span>
โ brew install llm
<span style="color:#75715e"># Install llm-ollama</span>
โ llm install llm-ollama
<span style="color:#75715e"># List all of models from Ollama</span>
โ llm ollama list-models
<span style="color:#75715e"># </span>
โ llm -m llama3.1:latest <span style="color:#e6db74">"how much is 2+2?"</span>
The answer to <span style="color:#ae81ff">2</span> + <span style="color:#ae81ff">2</span> is:
<span style="color:#ae81ff">4</span>
</code></pre></div><h2 id="bonus-mistral-large-2">Bonus: Mistral Large 2</h2>
<p>While I was working on this post, Mistral AI launched their <a href="https://mistral.ai/news/mistral-large-2407/">Large Enough: Mistral Large 2</a> model today. The Ollama project released <a href="https://ollama.com/library/mistral-large">support for the model</a> within minutes of its announcement.</p>
<p>The Mistral Large 2 release is noteworthy because it outperforms Lllama 3.1’s 405B parameter model and is under 1/3 of the size. It is also the second GPT-4 class model release in the last two days.</p>
<p>Check out Simon’s <a href="https://simonwillison.net/2024/Jul/24/mistral-large-2/">post</a> for more details and another LLM plugin for another way to access it.</p>
๐ Weeknotes for Week 28: July 8 to 14
https://micro.webology.dev/2024/07/20/weeknotes-for-week-july-to/
Sat, 20 Jul 2024 12:13:38 -0600http://webology.micro.blog/2024/07/20/weeknotes-for-week-july-to/<p><em>I’m running a week behind on this.</em></p>
<p>This week was our first week back home without traveling in a month, and it felt good to be home. I had time to catch up on mowing the yard, and I treated the yard with an eco-safe mosquito repellent. Despite the hot weather, Sunday felt nice outside to be mosquito-free.</p>
<p>I rolled my above-ground sprinkler system A few years ago, and I still need to install and run it this year. I wanted to get it this weekend, and here we are.</p>
<h2 id="family">Family</h2>
<p>I converted my daughter’s crib to a daybed over the weekend, and we have been using it for two nights and two naps without any issues. My son took to the board game Risk in Chicago, so I installed the iPad version and walked him through it. It was a pizza and tacos weekend because it was a long week.</p>
<h2 id="work">Work</h2>
<p>Occasionally, a project feels like you signed up for a race, but the distance keeps changing whenever you are within sight of the finish line. A project we have been finishing up keeps growing.</p>
<h2 id="community-work">Community Work</h2>
<ul>
<li>
<p><a href="https://djangonaut.space">Djangonaut Space</a>: All three of our Djangonauts completed their first tasks, and we are moving on to our first big group project.</p>
</li>
<li>
<p><a href="https://www.conferencechats.org">Conference Chats meetup</a></p>
</li>
</ul>
<h2 id="side-projects">Side projects</h2>
<ul>
<li>
<p><strong><a href="https://django-news.com">Django News Newsletter</a>:</strong> We shipped issue <a href="https://django-news.com/issues/241#start">#241</a>.</p>
</li>
<li>
<p><a href="https://jobs.django-news.com">Django News Jobs</a>: This week, we picked up more jobs that weren’t from one source. I need to write a tool to help maintain this, but it’s a manageable load.</p>
</li>
<li>
<p>I bought a new domain name for a project this weekend. More on that soon.</p>
</li>
</ul>
<h2 id="side-quests">Side Quests</h2>
<ul>
<li>
<p>I dusted off my YouTube-to-Frontmatter tool and added the ability to pull playlists from a given username. I wrote the files out as JSON and used DuckDB to query them, which worked amazingly well.</p>
</li>
<li>
<p>I wrote an Amazon product image downloader for a few blog posts. When the product API did not work, I punted and had ChatGPT write a playwright scraper. It was faster and much less frustrating. I need this for several projects.</p>
</li>
<li>
<p>I cleaned up my sitemaps research tool.</p>
</li>
<li>
<p>I tried out a screenshots-to-code project and ran some 00s-era websites through it that I wish still existed. If someone wants to give me a few years of funding, I think we can make the web not suck again.</p>
</li>
</ul>
<h2 id="writing">Writing</h2>
<p><a href="https://micro.webology.dev/2024/07/14/why-i-deleted.html">2024-07-14</a>:ย <strong>๐ฅ Why I deleted Firefox from my machines this weekend</strong>ย I no longer trust or believe in Mozilla, so I deleted Firefox from my machines this weekend. โ brew โฆ</p>
<p><a href="https://micro.webology.dev/2024/07/12/duckdb-may-be.html">2024-07-13</a>:ย <strong>๐ฆ DuckDB may be the tool you didn’t know you were missing</strong>ย ๐ค I havenโt fully figured out DuckDB yet, but itโs worth trying out if you are a Python โฆ</p>
<p><a href="https://micro.webology.dev/2024/07/12/macos-bartender-app.html">2024-07-12</a>:ย <strong>๐ macOS Bartender app to Ice app</strong>ย I upgraded my Macs to macOS Sonoma a few weeks ago. While everything has been uneventful, the โฆ</p>
<p><a href="https://micro.webology.dev/2024/07/11/bitdo-goldensilver-limited.html">2024-07-11</a>:ย <strong>๐ฎ 8BitDo Golden/Silver Limited Edition controllers</strong>ย My favorite third-party video game hardware company, 8BitDo, announced its 11th-anniversary limited โฆ</p>
<p><a href="https://micro.webology.dev/2024/07/10/office-hours-for.html">2024-07-10</a>:ย <strong>๐ Office Hours for July 12th</strong>ย Office Hours returns this Friday, July 12th, 2024, from 2:30 pm to 4:30 pm Central Time. โน๏ธ Anyone โฆ</p>
<p><a href="https://micro.webology.dev/2024/07/09/sharing-is-caring.html">2024-07-09</a>:ย <strong>๐ Sharing is Caring: How a Simple Sudo Question Led to Better Solutions</strong>ย One of the fun discoveries of blogging is finding your article in search results while trying to โฆ</p>
<p><a href="https://micro.webology.dev/2024/07/08/weeknotes-for-week.html">2024-07-08</a>:ย <strong>๐ Weeknotes for Week 27: July 1 to 7</strong>ย The last week was a blur between the holiday, travel, and cramming a lot of work. My notes this week โฆ</p>
<h2 id="entertainment">Entertainment</h2>
<p>๐บ <a href="https://trakt.tv/shows/vikings-valhalla">Vikings: Valhalla</a></p>
<p>๐บ <a href="https://trakt.tv/movies/the-marvels-2023">The Marvels (2023)</a> - This movie was better than people gave it credit for. It wasn’t my favorite, but it was fun to watch.</p>
<p>๐บ <a href="https://trakt.tv/shows/defending-jacob">Defending Jacob</a> - I skipped to the end of this series.</p>
<p>๐บ <a href="https://trakt.tv/shows/the-last-thing-he-told-me">The Last Thing He Told Me</a> - I skipped to the end of this series.</p>
<p>๐บ <a href="https://trakt.tv/shows/presumed-innocent">Presumed Innocent</a> - I surprised myself that I’m still keeping up with this series, but there are only a few weeks left.</p>
<p>๐บ <a href="https://trakt.tv/shows/the-acolyte">The Acolyte</a> - We are ready for the last episode.</p>
<p>๐บ <a href="https://trakt.tv/movies/atlas-2024">Atlas (2024)</a> - I didn’t go into this movie with any expectations, and I immensely enjoyed it.</p>
<h2 id="next-week">Next week</h2>
<p>I’m solo-parenting next weekend. I’m looking forward to hanging out with my kids and another weekend of being home.</p>
๐ณ๏ธ My thoughts on the PSF Election results
https://micro.webology.dev/2024/07/16/my-thoughts-on-the-psf/
Tue, 16 Jul 2024 21:37:50 -0600http://webology.micro.blog/2024/07/16/my-thoughts-on-the-psf/<p>A few weeks ago, I wrote about this year’s PSF Election, three <a href="https://micro.webology.dev/2024/07/02/on-the-psf.html">proposed bylaws changes</a>, and how I intended to vote. I’m happy that the membership overwhelmingly approved all three proposed bylaw changes. Here is this <a href="https://opavote.com/results/5004101476679680">year’s results</a>.</p>
<h2 id="merging-contributing-and-managing-member-classes">Merging Contributing and Managing member classes</h2>
<p>This change is a good step toward consolidating two membership classes and a commitment to acknowledging that all community contributions are important, not just code contributions.</p>
<h2 id="simplifying-the-voter-affirmation-process-by-treating-past-voting-activity-as-intent-to-continue-voting">Simplifying the voter affirmation process by treating past voting activity as intent to continue voting</h2>
<p>If you voted in last year’s election, there are fewer barriers to voting in the next election. With a 76% turnout this year, I suspect next year will still yield over a 50% voter turnout, and I suspect turnout will continue to be high.</p>
<h2 id="allow-for-removal-of-fellows-by-a-board-vote-in-response-to-code-of-conduct-violations-removing-the-need-for-a-vote-of-the-membership">Allow for removal of Fellows by a Board vote in response to Code of Conduct violations, removing the need for a vote of the membership</h2>
<p>This one means the most to me. When I joined the board, our Code of Conduct was barely two paragraphs long and said little. We rewrote it and formed the PSF Code of Conduct workgroup. From today forward, we can appreciate that the Python Code of Conduct applies to everyone.</p>
<h2 id="overall">Overall</h2>
<p>We also gained three new directors, including two returning directors. This election may be the first time we have had an election in which no one running from North America made it on the board. (Possibly Europe, too, but I didn’t dive as deep to verify that.) Either way, this is a noteworthy milestone.</p>
<p>I’m proud of the Python community for embracing our Code of Conduct and membership changes. A few of these were overdue, but updating the voter affirmation process is an excellent proactive step and a shift for the board.</p>
<p>I also want to thank Dรฉbora Azevedo, the PSF’s vice chair-elect and our outbound director. I was impressed with Dรฉbora when we served on the board together, and I thought she brought valuable insights. When she put her name forward to run for vice chair, I was impressed because it’s an intimidating group to put yourself out there, and I thought Dรฉbora managed it well.</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://opavote.com/results/5004101476679680">๐ณ๏ธ Python Software Foundation Board of Directors Election 2024 Results</a></li>
<li><a href="https://micro.webology.dev/2024/07/02/on-the-psf.html">๐ฌ On the PSF Bylaw changes</a></li>
<li><a href="https://micro.webology.dev/2024/07/03/psf-elections-how.html">๐ณ๏ธ PSF Elections how I am voting</a></li>
</ul>
๐ Django Migration Operations aka how to rename Models
https://micro.webology.dev/2024/07/15/django-migration-operations-aka-how/
Mon, 15 Jul 2024 11:20:41 -0600http://webology.micro.blog/2024/07/15/django-migration-operations-aka-how/<p>Renaming a table in Django seems more complex than it is. Last week, a client asked me how much pain it might be to rename a Django model from Party to Customer. We already used the model’s <code>verbose_name</code>, so it has been referencing the new name for months.</p>
<p>Renaming the model should be as easy as renaming the model while updating any foreign key and many-to-many field references in other models and then running Django’s <code>make migrations</code> sub-command to see where we are at.</p>
<p>The main issue with this approach is that Django will attempt to create a new table first, update model references, and then drop the old table.</p>
<p>Unfortunately, Django will either fail mid-way through this migration and roll the changes back or even worse, it may complete the migration only for you to discover that your new table is empty.</p>
<p>Deleting data is not what we want to happen.</p>
<p>As it turns out, Django supports a <a href="https://docs.djangoproject.com/en/5.0/ref/migration-operations/#renamemodel"><code>RenameModel</code></a> migration option, but it did not prompt me to ask if we wanted to rename Party to Customer.</p>
<p>I am also more example-driven, and the Django docs don’t have an example of how to use <code>RenameModel</code>. Thankfully, this migration operation is about as straightforward as one can imagine: <code>class RenameModel(old_model_name, new_model_name)</code></p>
<p>I re-used the existing migration file that Django created for me. I dropped the <code>CreateModel</code> and <code>DeleteModel</code> operations, added a <code>RenameField</code> operation, and kept the <code>RenameField</code> operations which resulted in the following migration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#f92672">from</span> django.db <span style="color:#f92672">import</span> migrations
<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Migration</span>(migrations<span style="color:#f92672">.</span>Migration):
dependencies <span style="color:#f92672">=</span> [
(<span style="color:#e6db74">'resources'</span>, <span style="color:#e6db74">'0002_alter_party_in_the_usa'</span>),
]
operations <span style="color:#f92672">=</span> [
migrations<span style="color:#f92672">.</span>RenameModel(<span style="color:#e6db74">'Party'</span>, <span style="color:#e6db74">'Customer'</span>),
migrations<span style="color:#f92672">.</span>RenameField(<span style="color:#e6db74">'Customer'</span>, <span style="color:#e6db74">'party_number'</span>, <span style="color:#e6db74">'customer_number'</span>),
migrations<span style="color:#f92672">.</span>RenameField(<span style="color:#e6db74">'AnotherModel'</span>, <span style="color:#e6db74">'party'</span>, <span style="color:#e6db74">'customer'</span>),
]
</code></pre></div><p>The story’s moral is that you should <em>always check and verify</em> that your Django migrations will perform as you expect before running them in production. Thankfully, we did, even though glossing over them is easy.</p>
<p>I also encourage you to dive deep into the areas of the Django docs where there aren’t examples. Many areas of the docs may need examples or even more expanded docs, and they are easy to gloss over or get intimidated by.</p>
<p>You don’t have to be afraid to create and update your migrations by hand. After all, Django migrations are Python code designed to give you a jumpstart. You can and should modify the code to meet your needs. <a href="https://docs.djangoproject.com/en/5.0/ref/migration-operations/">Migration Operations</a> have a clean API once you dig below the surface and understand what options you have to work with.</p>
๐ฆ DuckDB may be the tool you didn't know you were missing
https://micro.webology.dev/2024/07/13/duckdb-may-be-the-tool/
Sat, 13 Jul 2024 08:31:37 -0600http://webology.micro.blog/2024/07/13/duckdb-may-be-the-tool/<p>๐ค I haven’t fully figured out <a href="https://duckdb.org">DuckDB</a> yet, but it’s worth trying out if you are a Python dev who likes to work on data projects or gets frequently tasked with data import projects.</p>
<p>DuckDB is a fast database engine that lets you read CSV, Parquet, and JSON files and query them using SQL. Instead of importing data into your database, DuckDB enables you to write SQL and run it against these file types.</p>
<p>I have a YouTube to frontmatter project that can read a YouTube playlist and write out each video to a markdown file. I modified the export script to save the raw JSON output to disk.</p>
<p>I used DuckDB to read a bunch of JSON files using the following script:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#f92672">import</span> duckdb
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
result <span style="color:#f92672">=</span> duckdb<span style="color:#f92672">.</span>sql(<span style="color:#e6db74">"SELECT id,snippet FROM read_json('data/*.json')"</span>)<span style="color:#f92672">.</span>fetchall()
<span style="color:#66d9ef">for</span> row <span style="color:#f92672">in</span> result:
id, snippet <span style="color:#f92672">=</span> row
print(<span style="color:#e6db74">f</span><span style="color:#e6db74">"</span><span style="color:#e6db74">{</span>id<span style="color:#e6db74">=}</span><span style="color:#e6db74">"</span>)
print(snippet[<span style="color:#e6db74">"channelTitle"</span>])
print(snippet[<span style="color:#e6db74">"title"</span>])
print(snippet[<span style="color:#e6db74">"publishedAt"</span>])
print(snippet[<span style="color:#e6db74">"description"</span>])
print()
<span style="color:#66d9ef">if</span> __name__ <span style="color:#f92672">==</span> <span style="color:#e6db74">"__main__"</span>:
main()
</code></pre></div><p>This script accomplishes several things:</p>
<ul>
<li>It reads over 650 JSON files in about one second.</li>
<li>It uses SQL to query the JSON data directly.</li>
<li>It extracts specific fields (id and snippet) from each JSON file.</li>
</ul>
<h2 id="performance-and-ease-of-use">Performance and Ease of Use</h2>
<p>The speed at which DuckDB processes these files is remarkable. In traditional setups, reading and parsing this many JSON files could take significantly longer and require more complex code.</p>
<h2 id="when-to-use-duckdb">When to Use DuckDB</h2>
<p>DuckDB shines in scenarios where you need to:</p>
<ul>
<li>Quickly analyze data in files without a formal import process.</li>
<li>Perform SQL queries on semi-structured data (like JSON)</li>
<li>Process large datasets efficiently on a single machine.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>DuckDB is worth trying out in your data projects. If you have a lot of data and you need help with what to do with it, being able to write SQL against hundreds of files is powerful and flexible.</p>
๐ Weeknotes for Week 27: July 1 to 7
https://micro.webology.dev/2024/07/08/weeknotes-for-week-july-to/
Mon, 08 Jul 2024 11:37:40 -0600http://webology.micro.blog/2024/07/08/weeknotes-for-week-july-to/<p>The last week was a blur between the holiday, travel, and cramming a lot of work. My notes this week are more glossed over than most</p>
<h2 id="family">Family</h2>
<p>We drove to Illinois (near Chicago) to see family this week. We had family from both coasts who we don’t see very often, and it was the first time much of the family met my youngest, Nora. It takes us 8 to 8.5 hours to drive there, and people are always amazed at how good my kids are at traveling these distances. They both love the extra screen time and are easy to travel with. We can hand each one an iPad, and they are set for hours, given enough snacks and a rest stop every few hours.</p>
<h2 id="work">Work</h2>
<p>It was a short two-day week, but I got a bonus third day of work between fitting a few hours in the car and half a day on Friday. This was nice because we are wrapping up an existing client project while ramping up on another project.</p>
<p>This has also made me realize that Django has many shopping cart projects, but they all seem to be outdated. We struggled to find a project we could use, which made me realize that the Django community is sorely missing a good shopping cart and checkout experience that works.</p>
<h2 id="community-work">Community Work</h2>
<p><a href="https://djangonaut.space">Djangonaut Space</a>: All three Djangonaut Space Team Neptune members have made meaningful contributions early in the process, and now I’m feeling the pressure to detail and share some more advanced projects so they can have tasks for the duration of the project.</p>
<p>Django Code of Conduct WG: We have prioritized keeping up as we get new members used to contributing. This week, was a busy week.</p>
<h2 id="side-projects">Side projects</h2>
<p><strong><a href="https://django-news.com">Django News Newsletter</a>:</strong> We shipped issue <a href="https://django-news.com/issues/240#start">#240</a>.</p>
<p><a href="https://jobs.django-news.com">Django News Jobs</a>: We have had code to aggregate some other Django job boards, and this week, we started allowing those to come through.</p>
<h2 id="writing">Writing</h2>
<p>This week, I was back on track, and I wrote and published something every day. It was my first day of giving myself a buffer so that I am always writing tomorrow’s post, which took the edge and pressure off. I am still writing every day, but with the holiday, it was nice knowing I didn’t have to fit it in on our last day in Chicago, which ended up being a really long, full day with family.</p>
<p><a href="https://micro.webology.dev/2024/07/07/more-fun-with.html">2024-07-07</a>:ย <strong>๐งฐ More fun with Django Extensions using <code>shell_plus</code> and <code>graph_models</code></strong>ย Yesterday, I wrote about Django Extensions show_urls management command because itโs useful. I โฆ</p>
<p><a href="https://micro.webology.dev/2024/07/06/django-extensions-is.html">2024-07-06</a>:ย <strong>Django Extensions is useful even if you only use show_urls</strong>ย Yes, Django Extensions package is worth installing, especially for its show_urls command, which can โฆ</p>
<p><a href="https://micro.webology.dev/2024/07/05/no-office-hours.html">2024-07-05</a>:ย <strong>๐ No Office Hours on July 5th, but…</strong>ย No Office Hours this week (July 5th), but we will return next Friday, July 12th, 2024,ย 2:30 pm to โฆ</p>
<p><a href="https://micro.webology.dev/2024/07/04/happy-aindependence-day.html">2024-07-04</a>:ย <strong>๐ ๐ค Happy AIndependence Day</strong>ย To everyone in the United States, Happy Independence Day and Happy AIndependence Day to everyone โฆ</p>
<p><a href="https://micro.webology.dev/2024/07/03/psf-elections-how.html">2024-07-03</a>:ย <strong>๐ณ๏ธ PSF Elections how I am voting</strong>ย This was written while driving to Chicago (technically from the passenger seat). Still, a few people โฆ</p>
<p><a href="https://micro.webology.dev/2024/07/02/on-the-psf.html">2024-07-02</a>:ย <strong>๐ฌ On the PSF Bylaw changes</strong>ย The Python Software Foundation has three bylaw changes up for a vote in this yearโs election. โฆ</p>
<p><a href="https://micro.webology.dev/2024/07/01/weeknotes-for-week.html">2024-07-01</a>:ย <strong>๐ Weeknotes for Week 26: June 24 to 30</strong>ย Family I took two days off for a funeral and some other family stuff. I saw cousins and other โฆ</p>
<h2 id="entertainment">Entertainment</h2>
<p>๐บ <a href="https://trakt.tv/shows/sweet-tooth">Sweet Tooth</a> - I really liked this series.</p>
<p>๐บ <a href="https://trakt.tv/shows/presumed-innocent">Presumed Innocent</a> - I watched the first five episodes, and it’s okay.</p>
<p>๐บ <a href="https://trakt.tv/shows/the-last-thing-he-told-me">The Last Thing He Told Me</a> - I just started this series on Sunday night and am unsure if I’ll finish it.</p>
<h2 id="next-week">Next week</h2>
<p>It’s my first whole week back in a while. I’m looking forward to catching up and mowing my yard while listening to podcasts. I need to catch up on house stuff, including running my homemade above-ground sprinkler system. (Chill; we live near the Kansas River.)</p>
Django Extensions is useful even if you only use show_urls
https://micro.webology.dev/2024/07/06/django-extensions-is-useful-even/
Sat, 06 Jul 2024 08:00:00 -0600http://webology.micro.blog/2024/07/06/django-extensions-is-useful-even/<p>Yes, <a href="https://github.com/django-extensions/django-extensions">Django Extensions</a> package is worth installing, especially for its show_urls command, which can be very useful for debugging and understanding your project’s URL configurations.</p>
<p>Here’s a short example of how to use it because I sometimes want to include a link to the Django Admin in a menu for staff users, and I am trying to remember what name I need to reference to link to it.</p>
<p>First, you will need to install it via:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">pip install django-extensions
<span style="color:#75715e"># or if you prefer using uv like me:</span>
uv pip install django-extensions
</code></pre></div><p>Next, you’ll want to add <code>django_extensions</code> to your <code>INSTALLED_APPS</code> in your <code>settings.py</code> file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">INSTALLED_APPS <span style="color:#f92672">=</span> [
<span style="color:#f92672">...</span>
<span style="color:#e6db74">"django_extensions"</span>,
]
</code></pre></div><p>Finally, to urn the <code>show_urls</code> management command you may do some by running your <code>manage.py</code> script and passing it the following option:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ python -m manage show_urls
</code></pre></div><p>Which will give this output:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ python -m manage show_urls | grep admin
...
/admin/ django.contrib.admin.sites.index admin:index
/admin/<app_label>/ django.contrib.admin.sites.app_index admin:app_list
/admin/<url> django.contrib.admin.sites.catch_all_view
<span style="color:#75715e"># and a whole lot more...</span>
</code></pre></div><p>In this case, I was looking for <code>admin:index</code> which I can now add to my HTML document this menu link/snippet:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-html" data-lang="html">...
<<span style="color:#f92672">a</span> <span style="color:#a6e22e">href</span><span style="color:#f92672">=</span><span style="color:#e6db74">"{% url 'admin:index' %}"</span>>Django Admin</<span style="color:#f92672">a</span>>
...
</code></pre></div><p>What I like about this approach is that I can now hide or rotate the url pattern I’m using to get to my admin website, and yet Django will always link to the correct one.</p>
๐ณ๏ธ PSF Elections how I am voting
https://micro.webology.dev/2024/07/03/psf-elections-how-i-am/
Wed, 03 Jul 2024 07:30:00 -0600http://webology.micro.blog/2024/07/03/psf-elections-how-i-am/<p>This was written while driving to Chicago (technically from the passenger seat). Still, a few people contacted me and asked me how I vote for PSF directors, so I wanted to share.</p>
<p>If you can vote in the PSF election, please do so before <strong>Tuesday, July 16th, 2024, 2:00 p.m. UTC</strong>. For more details, check out their blog post, <a href="https://pyfound.blogspot.com/2024/07/the-2024-psf-board-election-is-open.html">The 2024 PSF Board Election is Open!</a> (The blog post lists the date incorrectly as ending on a Friday.)</p>
<p>I served on the PSF board for five years, from 2018 to 2023, and here is what I am looking for when I research who to vote for this year.</p>
<p>It’s a harder-than-normal slate of candidates because only three open seats are available, compared to four in the last few years. More candidates are new to me this year than most years.</p>
<p>We also have some solid candidates running, which makes it even harder.</p>
<h2 id="existing-directors">Existing directors</h2>
<p>For existing directors, I look at:</p>
<ul>
<li>How long have they served on the board?</li>
<li>What positions/roles did they serve while on the board?</li>
<li>What has the PSF accomplished during its term?</li>
<li>What was their meeting attendance like?</li>
<li>I read their previous candidate statements to see if the PSF accomplished what they said they wanted.</li>
<li>How did they treat the PSF’s staff in meetings, at events, and behind the scenes?</li>
</ul>
<h2 id="all-candidates">All candidates</h2>
<p>For everyone, including existing directors, I read their candidate statements, and then I look at:</p>
<ul>
<li>What do they value?</li>
<li>What their company and community affiliations are.</li>
<li>Understands and is committed to our Code of Conduct to promote a healthy community.</li>
<li>What workgroups did they participate in?</li>
<li>What are they committed to working on and changing in their next term?</li>
</ul>
<h2 id="what-im-prioritizing">What I’m prioritizing</h2>
<p>I prioritize diverse candidates and representation. This includes geographical representation.</p>
<p>I prioritize candidates who <strong>do not work</strong> for Big Tech / FAANG companies. These companies often give raises and promotions to employees who make it on open-source boards. It’s a KPI goal for some Dev Rels and I’m not here to help any Big Tech companies. Thankfully, I have served with many selfless directors, despite them working for Big Tech companies. Still, you have a right to know when you vote for someone if their position will check off one of their KPIs.</p>
<p>I prioritize the Python community’s needs over individual needs.</p>
<p>I prioritize candidates with a track record of getting things done over only showing up to be seen. (Yes, this is a thing.)</p>
<p>I prioritize practical communication skills. If you write over the heads of the community, then you could be more effective at communicating.</p>
<h2 id="im-looking-for-reform">I’m looking for reform.</h2>
<p>Overall, I am looking for reform. A few public mishaps by the board have damaged the community’s trust in the organization.</p>
<p>I want to see reform across those impacted workgroups, and I will prioritize candidates who show awareness of this. I considered listing them here, but “what I’m prioritizing” when I vote is not up for public debate. I might write more on this later.</p>
<h2 id="if-you-are-running">If you are running</h2>
<p>If you are running for the board, thank you for putting yourself out there.</p>
๐ฌ On the PSF Bylaw changes
https://micro.webology.dev/2024/07/02/on-the-psf-bylaw-changes/
Tue, 02 Jul 2024 07:00:00 -0600http://webology.micro.blog/2024/07/02/on-the-psf-bylaw-changes/<p>The Python Software Foundation has three bylaw changes up for a vote in this year’s election. I support all there.</p>
<p>Here is their post, <a href="https://pyfound.blogspot.com/2024/06/for-your-consideration-proposed-bylaws.html">For your consideration: Proposed bylaws changes to improve our membership experience</a> and a follow-up post <a href="https://pyfound.blogspot.com/2024/06/faq-for-proposed-changes-to-psf-bylaws.html">FAQ for Proposed Changes to PSF Bylaws</a> that addresses questions that came up.</p>
<h2 id="change-1-merging-contributing-and-managing-member-classes">Change 1: Merging Contributing and Managing member classes</h2>
<p>The existing two classes need to be clarified and updated. We want contributors, and code is only one of many ways to contribute to a healthy community.</p>
<p>โ I’m all for this. I support this change.</p>
<h2 id="change-2-simplifying-the-voter-affirmation-process-by-treating-past-voting-activity-as-intent-to-continue-voting">Change 2: Simplifying the voter affirmation process by treating past voting activity as intent to continue voting</h2>
<p>Before the voting affirmation process was enforced, meeting the 30% quorum mark was an issue.</p>
<p>Last year, over 70% of our members voted, and I suspect we can land somewhere in the middle, letting people continue to vote if they did the previous year and still staying above 50% of our active membership voting.</p>
<p>โ I support this change.</p>
<h2 id="change-3-allow-for-removal-of-fellows-by-a-board-vote-in-response-to-code-of-conduct-violations-removing-the-need-for-a-vote-of-the-membership">Change 3: Allow for removal of Fellows by a Board vote in response to Code of Conduct violations, removing the need for a vote of the membership</h2>
<p>No one in the Python community is above Python’s Code of Conduct.</p>
<p>While I disagree that the board cannot already remove a Fellow, it’s better to bring it to the community for a vote.</p>
<p>โ As a Python Fellow, past board member, and Code of Conduct WG member, I support this change.</p>
๐ Weeknotes for Week 26: June 24 to 30
https://micro.webology.dev/2024/07/01/weeknotes-for-week-june-to/
Mon, 01 Jul 2024 08:30:00 -0600http://webology.micro.blog/2024/07/01/weeknotes-for-week-june-to/<h2 id="family">Family</h2>
<p>I took two days off for a funeral and some other family stuff. I saw cousins and other relatives who I only see at funerals, which is starting to feel like a habit.</p>
<h2 id="work">Work</h2>
<p>I was mentally blocked on a project, or at least the part I was putting off returning to. Something finally clicked over the weekend, and I worked through it. I hate working the weekends, which our company frowns upon. I didn’t expect to be out two days this week, and I’m out for the holiday most of the next week, so I tried to get caught up and ahead.</p>
<h2 id="community-work">Community Work</h2>
<p><a href="https://djangonaut.space">Djangonaut Space</a>: Week 2 kicked off, and I had to push our weekly standup back a week due to the funeral. No idea, but our Djangonauts are doing good work and working through their first issues.</p>
<h2 id="side-projects">Side projects</h2>
<p><strong><a href="https://django-news.com">Django News Newsletter</a>:</strong> We shipped issue <a href="https://django-news.com/issues/239#start">#239</a>.</p>
<p><a href="https://jobs.django-news.com">Django News Jobs</a>: I had a weird issue that forced me to downgrade Python 3.12 to 3.11 to fix something internally that broke Pydantic. I’ll switch back in a few weeks, but I’m still annoyed by it.</p>
<p>Overall, I just treaded water this week.</p>
<h2 id="writing">Writing</h2>
<p>I shifted my writing schedule around this weekend so that I am a day ahead. I want to get four or five days ahead so that I have a nice publishing schedule to work with. I still plan on writing daily, but I like having more wiggle room.</p>
<p><a href="https://micro.webology.dev/2024/06/30/using-just-and.html">2024-06-30</a>:ย <strong>๐ณ Using Just and Compose for interactive Django and Python debugging sessions</strong>ย When I wrote REST APIs, I spent weeks and months writing tests and debugging without looking at the โฆ</p>
<p><a href="https://micro.webology.dev/2024/06/29/docker-postgres-autoupgrades.html">2024-06-29</a>:ย <strong>๐ Docker Postgres Autoupgrades</strong>ย Upgrading Postgres in Docker environments can be daunting, but keeping your database up-to-date is โฆ</p>
<p><a href="https://micro.webology.dev/2024/06/28/a-just-recipe.html">2024-06-28</a>:ย <strong>๐ A Just recipe to backup and restore a Postgres database</strong>ย I have used this casey/just recipe to help backup and restore my Postgres databases from my Docker โฆ</p>
<p><a href="https://micro.webology.dev/2024/06/27/office-hours-for.html">2024-06-27</a>:ย <strong>๐ Office Hours for June 28th</strong>ย Itโs been a week, but Iโm hosting Office Hours this Friday at 2:30 pm. I was on the โฆ</p>
<p><a href="https://micro.webology.dev/2024/06/25/managing-docker-compose.html">2024-06-25</a>:ย <strong>๐ณ Managing Docker Compose Profiles with Just: Switching Between Default and Celery Configurations</strong>ย For a recent client project, we wanted to toggle between various Docker Compose profiles to run the โฆ</p>
<p><a href="https://micro.webology.dev/2024/06/24/mastodon-bookmark-exporter.html">2024-06-24</a>:ย <strong>๐ Mastodon Bookmark exporter to Markdown/Frontmatter</strong>ย I wrote a Mastodon Bookmark exporter tool over the weekend and decided to polish it up and release โฆ</p>
<h2 id="entertainment">Entertainment</h2>
<p>๐บ <a href="https://trakt.tv/shows/presumed-innocent">Presumed Innocent</a></p>
<p>๐บ <a href="https://trakt.tv/shows/baby-reindeer">Baby Reindeer</a></p>
<p>๐บ <a href="https://trakt.tv/shows/dark-matter-2024">Dark Matter</a></p>
<p>๐บ <a href="https://trakt.tv/shows/the-acolyte">The Acolyte</a> - We got caught up, and OMG, it’s good.</p>
<p>๐บ <a href="https://trakt.tv/movies/the-flash-2023">The Flash (2023)</a> - I couldn’t care less for the Flash, but I really enjoyed seeing Michael Keaton’s Batman again.</p>
<h2 id="next-week">Next week</h2>
<p>We are spending our holiday in Illinois to see family. It’ll be the first time my youngest has met several family members flying in for the holiday.</p>
๐ณ Using Just and Compose for interactive Django and Python debugging sessions
https://micro.webology.dev/2024/06/30/using-just-and-compose-for/
Sun, 30 Jun 2024 08:00:00 -0600http://webology.micro.blog/2024/06/30/using-just-and-compose-for/<p>When I wrote REST APIs, I spent weeks and months writing tests and debugging without looking at the front end. It’s all JSON, after all.</p>
<p>For most of my projects, I will open two or three tabs. I’m running Docker Compose in tab one to see the logs as I work. I’ll use the following <a href="https://github.com/casey/just">casey/just</a> recipe to save some keystrokes and to standardize what running my project looks like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#75715e"># tab 1</span>
$ just up
</code></pre></div><p>In my second tab, I’ll open a shell that is <strong>inside</strong> my main web or app container so that I can interact with the environment, run migrations, and run tests.</p>
<p>We can nitpick the meaning of “console” here, but I tend to have another just recipe for “shell” which will open a Django shell using shell_plus or something more interactive:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#75715e"># tab 2</span>
$ just console
</code></pre></div><p>In my third tab, I’ll run a shell session for creating git branches, switching git branches, stashing git changes, and running my linter, which I prefer to run by hand.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#75715e"># tab 3</span>
$ echo <span style="color:#e6db74">"I'm boring"</span>
</code></pre></div><p>Over the last year or two, the web has returned to doing more frontend work with Django and less with REST. Using <code>ipdb</code>, in my view, to figure out what’s going on has been really helpful. Trying to get <code>ipdb</code> to “just work” takes a few steps in my normal workflow.</p>
<pre tabindex="0"><code># tab 1 (probably)
# start everything
$ just start
# stop our web container
$ just stop web
# start our web container with "--service-ports"
# just start-web-with-debug
</code></pre><p>The only real magic here is using Docker’s <code>--service-ports</code>, which opens ports so we may connect to the open <code>ipdb</code> session when we open one in our view code.</p>
<p>My main <code>justfile</code> for all of these recipes/workflows looks very similar to this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yml" data-lang="yml"><span style="color:#75715e"># justfile</span>
<span style="color:#ae81ff">set dotenv-load := false</span>
<span style="color:#f92672">@build *ARGS</span>:
<span style="color:#ae81ff">docker compose build {{ ARGS }}</span>
<span style="color:#75715e"># opens a console</span>
<span style="color:#f92672">@console</span>:
<span style="color:#ae81ff">docker compose run --rm --no-deps utility/bin/bash</span>
<span style="color:#f92672">@down</span>:
<span style="color:#ae81ff">docker compose down</span>
<span style="color:#f92672">@start *ARGS</span>:
<span style="color:#ae81ff">just up --detach {{ ARGS }}</span>
<span style="color:#f92672">@start-web-with-debug</span>:
<span style="color:#ae81ff">docker compose run --service-ports --rm web python -m manage runserver 0.0.0.0:8000</span>
<span style="color:#f92672">@stop *ARGS</span>:
<span style="color:#ae81ff">docker compose down {{ ARGS }}</span>
<span style="color:#f92672">@up *ARGS</span>:
<span style="color:#ae81ff">docker compose up {{ ARGS }}</span>
</code></pre></div><p>If you work on multiple projects, I encourage you to find patterns you can scale across them. Using Just, Make, shell scripts or even Python lightens the cognitive load when switching between them.</p>
๐ Mastodon Bookmark exporter to Markdown/Frontmatter
https://micro.webology.dev/2024/06/24/mastodon-bookmark-exporter-to-markdownfrontmatter/
Mon, 24 Jun 2024 22:21:16 -0600http://webology.micro.blog/2024/06/24/mastodon-bookmark-exporter-to-markdownfrontmatter/<p>I wrote a Mastodon Bookmark exporter tool over the weekend and decided to polish it up and release it tonight.</p>
<p>I wrote the tool to help me sort out Mastodon posts that I might bookmark to follow up on or write about. I bookmark posts on the go or even from bed, and when I have time, I will pull them back up.</p>
<p>The Mastodon Bookmark exporter tool reads your Mastodon bookmarks and exports the latest posts to a markdown/frontmatter file.</p>
<p>I’m releasing the project as a <a href="https://gist.github.com/jefftriplett/623039d2fd09b959eadde6a496ade47a">gist</a> under the <a href="https://polyformproject.org/licenses/noncommercial/1.0.0">PolyForm Noncommercial License</a> for personal reasons. If you have licensing questions, contact me directly or through <a href="https://www.revsys.com">www.revsys.com</a> for commercial inquiries, and we can work something out.</p>
<script src="https://gist.github.com/jefftriplett/623039d2fd09b959eadde6a496ade47a.js"></script>
Weeknotes for Week 25: June 17 to 23
https://micro.webology.dev/2024/06/23/weeknotes-for-week-june-to/
Sun, 23 Jun 2024 21:38:34 -0600http://webology.micro.blog/2024/06/23/weeknotes-for-week-june-to/<p>Last week, my side of the family drove out to Colorado Springs, Colorado, and we rented a house for the week. The trip was great; seeing family was good, and watching the kids spend time with family they don’t get to see very often was the highlight for me. The mountains were great, but I don’t want to ride a train from 8k to 14k feet of altitude soon.</p>
<h2 id="family">Family</h2>
<p>The last of my biological aunts and uncles passed away two nights before we left Colorado. We had some other unexpected news, and I hope we have better news after the funeral this week.</p>
<h2 id="work">Work</h2>
<p>I took the whole week off, so the only work this week was community work.</p>
<h2 id="community-work">Community Work</h2>
<p><a href="https://djangonaut.space">Djangonaut Space</a>: Week 1 kicked off, and we had our first team meeting. The meeting went well, and I have a follow-up meeting on Monday with one Djangonaut who couldn’t make it.</p>
<p>General Python: I had a ton of messages from Python friends this week asking for advice on upcoming bylaws changes. I wrote up a few drafts, and I might get around to publishing one or the other.</p>
<h2 id="side-projects">Side projects</h2>
<p><strong><a href="https://django-news.com">Django News Newsletter</a>:</strong> We shipped issue <a href="https://django-news.com/issues/238#start">#238</a>.</p>
<p><a href="https://djangopackages.org">Django Packages</a>: More small updates and more on this sooner.</p>
<p>I wrote a tool to export my Mastodon bookmarks. I tend to revisit these every day, and I wanted a way to create checklists for them and save them. I might find good content from the newsletter or posts I want to follow up on once I have more time.</p>
<h2 id="writing">Writing</h2>
<p>I wrote every day but only published for three days, including today. I wrote several drafts, but I mostly spent time with my family.</p>
<p><a href="https://micro.webology.dev/2024/06/19/follow-black-python.html">2024-06-19</a>:ย <strong>๐ Follow Black Python Devs on Juneteenth</strong>ย If you havenโt heard about Black Python Devs, Juneteenth is an excellent day to learn about โฆ</p>
<p><a href="https://micro.webology.dev/2024/06/17/djangonaut-space-has.html">2024-06-17</a>:ย <strong>Djangonaut Space has begun</strong>ย This is my first time helping out with Djangonaut Space as a Navigator for the Django Packages…</p>
<h2 id="entertainment">Entertainment</h2>
<p>๐บ <a href="https://trakt.tv/movies/leave-the-world-behind-2023">Leave the World Behind</a> - I rewatched half of this movie with my mother and sister.</p>
<p>๐บ <a href="https://trakt.tv/movies/hellboy-2019">Hellboy</a> - I didn’t realize I had missed this one. I liked it even if I don’t know much about the Hellboy story outside the other movies.</p>
<h2 id="next-week">Next week</h2>
<p>I am returning to work a day earlier than planned, but it will be a long, short week.</p>
๐ Follow Black Python Devs on Juneteenth
https://micro.webology.dev/2024/06/19/follow-black-python-devs-on/
Wed, 19 Jun 2024 12:47:59 -0600http://webology.micro.blog/2024/06/19/follow-black-python-devs-on/<p>If you haven’t heard about <a href="https://blackpythondevs.com">Black Python Devs</a>, Juneteenth is an excellent day to learn about this organization and how to support it.</p>
<p>If you haven’t heard about Black Python Devs, here is a quick overview:</p>
<blockquote>
<p>Black Python Devs was created by its founder Jay Miller after seeing a trend of the same handful of Black developers speaking at major conferences, taking leadership positions, and dealing with the same challenges towards burnout.</p>
<p><a href="https://blackpythondevs.com/about/">blackpythondevs.com/about/</a></p>
</blockquote>
<p>I also want to share the goals of Black Python Devs:</p>
<blockquote>
<p>Our goal is to become the largest community of Black Python Developers in the world and establish our community as a source for diverse leaders in the greater Python community.</p>
<p>We aim to:</p>
<ul>
<li>Establish guidance, mentorship, and career support for Black Pythonistas around the world.</li>
<li>Create opportunities for the Python community to invest in local communities of Black Python Devs members</li>
<li>Increase participation of Black Python Devs members in existing Python community programs, events, and initiatives.</li>
</ul>
</blockquote>
<h2 id="follow-them-today">Follow them today</h2>
<p>Juneteenth is also a wonderful day to follow them on <a href="https://mastodon.social/@blackpythondevs">Mastodon</a> (248 followers) or on <a href="https://x.com/blackpythondevs">Xwitter</a> (606 followers).</p>
<p><strong>I would love to check these numbers tomorrow and see a non-zero increase in followers.</strong></p>
<p>You can also financially support Black Python Devs through the GNOME Foundation by scrolling down to the <a href="https://blackpythondevs.com">Support Black Python Devs</a> section of the website.</p>
<h2 id="disclosure">Disclosure</h2>
<p>I am a Black Python Devs Leadership Advisor who has watched and supported this organization grow even before I was asked to join. This team is near and dear to my heart, and I invite you to follow them and help spread the message.</p>
๐ฆ Exploring Ollama: An Open-Source LLM (my notes)
https://micro.webology.dev/2024/06/11/exploring-ollama-an-opensource-llm/
Tue, 11 Jun 2024 20:57:51 -0600http://webology.micro.blog/2024/06/11/exploring-ollama-an-opensource-llm/<p>I have meant to do a proper write-up on the <a href="https://ollama.com">Ollama</a> project for a while. Instead of putting it off any longer, I decided to publish my outline and notes, hoping that someone might find it helpful if you wanted to run your own local, offline LLM.</p>
<h2 id="why-ollama">Why Ollama?</h2>
<ul>
<li>Open-source LLM server</li>
<li>Runs over two dozen models, with new ones released every week</li>
<li>My personal favorite: <a href="https://ollama.com">Ollama</a></li>
</ul>
<h2 id="my-journey-with-ollama">My Journey with Ollama</h2>
<ul>
<li>Discovered Ollama a year ago</li>
<li>One of the first accessible projects for M-series Mac</li>
<li>User-friendly and performed well.</li>
</ul>
<h2 id="ollamas-growing-compatibility">Ollama’s Growing Compatibility</h2>
<ul>
<li>Expanded to support Windows and Linux</li>
<li>Became AMD-friendly</li>
<li>More accessible to users with various hardware setups</li>
</ul>
<h2 id="the-llama3-model-changes-everything">The Llama3 model changes everything</h2>
<ul>
<li><a href="https://ollama.com/library/llama3">Llama3</a> 70b model introduction</li>
<li>70b model runs on Mac Studio with 64 GB of RAM</li>
<li>Enables running a powerful LLM locally</li>
<li>Benefits: privacy, customization, offline access</li>
</ul>
<h2 id="making-life-easier-with-the-llm-python-project">Making Life Easier with the LLM Python Project</h2>
<ul>
<li>Installed <a href="https://github.com/taketwo/llm-ollama">llm-ollama</a> plugin into <a href="https://github.com/simonw/llm">LLM</a> Python project</li>
<li>Simplifies switching between different LLMs</li>
<li>Quicker and easier testing of new models compared to Ollama’s CLI</li>
</ul>
<h2 id="ollamas-library-and-apis">Ollama’s Library and APIs</h2>
<ul>
<li><a href="https://github.com/ollama/ollama-python">ollama-python</a> library for integrating Ollama models into Python projects</li>
<li>Ollama’s built-in <a href="https://github.com/ollama/ollama/blob/main/docs/openai.md">OpenAI compatible API</a> for seamless use with existing OpenAI-based applications</li>
</ul>
<h2 id="final-thoughts">Final Thoughts</h2>
<ul>
<li>Ollama: a top choice for open-source, locally-run LLM</li>
<li>Expanding compatibility</li>
<li>Impressive Llama3 model</li>
<li>Easy integration with LLM Python project</li>
<li>Handy libraries and APIs</li>
<li>Recommended for researchers, developers, and curious individuals</li>
</ul>
๐งฑ Django ModelForm Template starting point
https://micro.webology.dev/2024/06/08/django-modelform-template-starting-point/
Sat, 08 Jun 2024 22:56:56 -0600http://webology.micro.blog/2024/06/08/django-modelform-template-starting-point/<p>Recently, I have been doing a lot of Django formwork. I start with a basic template like <code>form.as_div</code> or <code>form|crispy</code> until it grows uncomfortable.</p>
<p>Today, I was bouncing between two projects, and I noticed I was working on the tasks that had grown uncomfortable to the point that I dreaded working on the templates.</p>
<p>While I enjoy working with Django’s template system, I was putting off these tasks, and all they had in common was finishing some of the form and template work.</p>
<p>I couldn’t quite understand why this was such a mental blocker, so I stopped working, disconnected, and mowed my yard. Thankfully, that did the trick.</p>
<p>As I finished mowing, I realized that I was struggling to complete these tasks because I was overwhelmed by needing to dump all the form fields into a template.</p>
<p>Once I realized why I was feeling this resistance, I realized I needed to focus on solving this issue to move on.</p>
<p>I remembered Daniel Roy Greenfeld’s <a href="https://daniel.feldroy.com/posts/2024-05-rapidly-creating-smoke-tests-for-django-views">Rapidly creating smoke tests for Django views</a> from a few weeks ago, where he made a management command to print out a bunch of smoke tests.</p>
<p>I decided to try the same technique by passing a string path to a Django ModelForm and printing out my form template.</p>
<p><strong>Edit:</strong> <a href="https://micro.blog">micro.blog</a> did not like the templates in templates from my Python script, so I had to swap out the inline example with a gist. Sorry about that.</p>
<script src="https://gist.github.com/jefftriplett/3ca8bb4a4b97bdecec3a257ff5cd1cf9.js"></script>
<p>This template could be better, but it was good enough. I tested it on a few of the forms I’m using on <a href="https://jobs.django-news.com">Django News Jobs</a>, and it’s an improvement over what I started with.</p>
<p>Something was in the water because when I checked our company Slack, Frank Wiles showed me his new <a href="https://github.com/frankwiles/make-management-command">make-management-command</a> project, which takes a similar approach to creating the folders and files needed to create a new management command.</p>
๐ฟ On the Google leak
https://micro.webology.dev/2024/06/03/on-the-google-leak/
Mon, 03 Jun 2024 18:37:29 -0600http://webology.micro.blog/2024/06/03/on-the-google-leak/<p>Last week, Google leaked 2,500 pages of internal documentation by accidentally pushing these docs to their GoogleAPI GitHub repo.</p>
<blockquote>
<p>A collection of 2,500 leaked internal documents from Google filled with details about data the company collects is authentic, the company confirmed today. Until now, Google had refused to comment on the materials.</p>
</blockquote>
<p>These documents were also leaked Apache 2.0 “irrevocable copyright license” when released under normal circumstances.</p>
<blockquote>
<p>The fun thing about accidentally publishing to the GoogleAPI GitHub is that, while these are sensitive internal documents, Google technically released them under an Apache 2.0 license. That means anyone who stumbled across the documents was granted a “perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license” to them, so these are freely available online now, like here.</p>
<p><a href="https://www.theverge.com/2024/5/29/24167407/google-search-algorithm-documents-leak-confirmation">www.theverge.com/2024/5/29…</a></p>
</blockquote>
<p>Several SEO experts have proven that Google has lied to web developers for over a decade. While I don’t have any remorse for the SEO industry, knowing “best practices” is good for anyone who builds a website and wants a chance to reasonably rank.</p>
<p>So, while Google has been called out for years for misleading the general public, now a trove of documents prove it.</p>
<blockquote>
<p>Both Fishkin and King accuse Google of “lying” to SEO experts in the past. One of the revelations in the documents is that the click-through rate of a search result listing affects its ranking, something Google has denied goes into the results “stew” on several occasions. The click tracking system is called “Navboost,” in other words, boosting websites users navigate to. Naturally, a lot of this click data comes from Chrome, even when you leave search. For instance, some results can show a small set of “sitemap” results below the main listing, and apparently a part of what powers this is the most-popular subpages as determined by Chrome’s click tracking.
<a href="https://arstechnica.com/gadgets/2024/06/google-accidentally-published-internal-search-documentation-to-github/">arstechnica.com/gadgets/2…</a></p>
</blockquote>
<p>As a web developer, what’s frustrating is how obvious spammy some of the top websites are that mirror websites like <a href="https://www.python.org">python.org</a>’s content and add spammy ads and content and somehow outrank the biggest programming language on the planet’s docs.</p>
<p>None of this is new. I have seen the same spammy websites for years, which led me to drop Google Search for <a href="kagi.com">Kagi</a> so I could stop seeing them.</p>
<p>Behavior never lies.</p>
๐ผ Packing and unpacking
https://micro.webology.dev/2024/05/20/packing-and-unpacking/
Mon, 20 May 2024 20:06:16 -0600http://webology.micro.blog/2024/05/20/packing-and-unpacking/<p>I’m exhausted after an excellent PyCon US, but I had packing and unpacking on my mind. I usually pack too many clothes in a suitcase that is too small and regretting it on my way back.</p>
<p>I like to use packing cubes, and I still pack way too much into slightly too small of a suitcase, and then I regret it after bringing home too much swap home with me.</p>
<p>This year, I challenged myself to pack less and to bring my more giant suitcase. I could even see the bottom of my luggage around my packing cubes.</p>
<p>Overall, my take-home booth/vendor swag was very modest this year. I only brought home one T-shirt, a hat, and a bunch of stickers. I meant to buy a t-shirt to support <a href="https://blackpythondevs.com">Black Python Devs</a>, but y’all bought them all before I could make it back with cash.</p>
<p>I also want to thank my friends who brought gifts for my kids and myself, which I didn’t see coming. I am humbled and amazed that my community work and friendship sparked that response. I am humbled, and everything fits perfectly in my suitcase.</p>
<p>My only misses during this trip were bringing too many USB-C cables, one book I never opened, and a smaller satchel to carry my laptop. I even left my computer in my hotel for a few days. Next year, I will bring fewer cables with me.</p>
<p>The only device I wish I had brought was a small power strip. I have a GHome Smart Power Strip, which has a few USB charger ports, and it would have been nicer than fighting some of the hotel’s loose, built-in chargers. With a power strip, I would have had more space to plug it into a wall jack.</p>
<p>Someday, I will write up a proper gear post since I picked up a few new items I enjoyed having this year, but tonight, I will start recovering from my sleep debt.</p>
๐ PyCon US, have it your way (you rule!)
https://micro.webology.dev/2024/05/16/pycon-us-have-it-your/
Thu, 16 May 2024 08:40:06 -0600http://webology.micro.blog/2024/05/16/pycon-us-have-it-your/<p>Here is my PyCon US advice based on Trey Hunner’s Mastodon post:</p>
<blockquote>
<p>What are some of your favorite @pycon tips? ๐ก</p>
<p>Reply and tag #PyConUS. ๐</p>
<p><a href="https://mastodon.social/@treyhunner/112448459788776426">mastodon.social/@treyhunn…</a></p>
</blockquote>
<h2 id="talks-are-recorded">Talks are recorded</h2>
<p>Keep in mind that talks are recorded.</p>
<p>Seeing every talk at PyCon US while you are attending is impossible. There are five tracks with three days of talks. There will be times when the two or three talks you most want to attend will all be given during the same block. Or you aren’t used to talking to people all day and need extra sleep.</p>
<p>It’s fine. The talks are recorded.</p>
<h2 id="ignore-pycon-us-advice-anti-patterns">Ignore PyCon US advice anti-patterns</h2>
<p>Python developers often have firm opinions. Every year, there are several blog posts about why you should “only go to talks” or “only hallway track” when the rest of us will do both.</p>
<h2 id="dont-sleep-on-the-first-two-days-of-the-expo-hall">Don’t sleep on the first two days of the Expo Hall</h2>
<p>The Expo Hall has sponsor booths for one reception evening and the first two days of talks. On the third day of talks, the Expo hall gets converted into a Job Fair with other events running, and it takes a lot of work to bump into people.</p>
<p>There have been some years where I never made it out of the Expo Hall for either day, even though I wasn’t working a booth. I have even watched lunch get carted in and carted out while talking with people because it’s a great location to meet new people and run into everyone you know online or from other conferences over the years. Don’t do that. Stop and eat lunch.</p>
<p>Take advantage of the first two days of the Expo Hall, even if you can’t make it to the opening reception. Say hi to sponsors, too.</p>
<h2 id="embrace-jomo-joy-of-missing-out">Embrace JOMO (Joy Of Missing Out)</h2>
<p>At my first PyCon US attendance as a board director, I didn’t know I was supposed to attend a dinner for PyCon US organizers. My plane arrived late, and I got pinged if I knew about it about 30 minutes after the dinner started. I did a quick check. It was a 20-minute walk in a city I just landed in, so I thanked them and told them I couldn’t make it.</p>
<p>Instead, I went to the hotel bar to get dinner at the bar. I wasn’t even staying in the main conference hotel. I made small talk with one of the other patrons, and they were not only attendees, but they just happened to be maintainers of a few of my favorite Python packages.</p>
<p>You’d be amazed at how many people you’d otherwise never have met, even when you feel like not saying anything on the beaten path.</p>
<h2 id="remember-burger-kings-golden-rule-have-it-your-way">Remember Burger King’s golden rule, “have it your way”</h2>
<p>My six-year-old loves the Burger King “You Rule” commercials because of their TV jingle. Even though he’s never had their food, that doesn’t stop him from singing the ending chorus, “At BK, have it your way,” and then shouting, “(You rule!).”</p>
<blockquote>
<p>You rule
You’re seizing the day
At BK, have it your way
(You rule!)
<a href="https://genius.com/Burger-king-you-rule-lyrics">genius.com/Burger-ki…</a></p>
</blockquote>
<p>While I can’t speak for their food, it’s good advice.</p>
<blockquote>
<p>You rule</p>
</blockquote>
<p>First, everyone likes a compliment and “you rule.” You probably don’t deflect a compliment when a TV jingle gives you one, so carry this into life the next time someone compliments you. Don’t deflect it.</p>
<blockquote>
<p>You’re seizing the day</p>
</blockquote>
<p>While PyCon US feels like a lot of days, it goes quickly. Take time to enjoy it. I have been to enough PyCons to know that jobs, careers, families, budgets, and health change.</p>
<blockquote>
<p>At BK, have it your way</p>
</blockquote>
<p>The only proper way to PyCon US is to “have it your way.” Go to the talks you want to see and talk to the people you want to talk to, but I encourage you to put yourself out there and meet new people.</p>
<blockquote>
<p>(You rule!)</p>
</blockquote>
<p>You rule!</p>
<p>(take the damn compliment)</p>
๐ Weeknotes for Week 19: May 6 to May 12
https://micro.webology.dev/2024/05/12/weeknotes-for-week-may-to/
Sun, 12 May 2024 19:21:09 -0600http://webology.micro.blog/2024/05/12/weeknotes-for-week-may-to/<p>What a week.</p>
<h2 id="family">Family</h2>
<p>My youngest was home most of the week with a virus, which changed everything about my week.</p>
<p>We had a rare night out to see <a href="https://micro.webology.dev/2024/05/07/223140.html">Jason Isbell and the 400 Unit at Liberty Hall</a></p>
<p>My family also had a birthday on Friday, my son’s friend’s birthday party on Saturday, and Mother’s Day today, so it was a busier than usual week for us and a sick kid all week.</p>
<h2 id="work">Work</h2>
<p>With my daughter sick, everything changed this week. I mostly worked on an e-commerce app on a rare Saturday to get my bearings.</p>
<h2 id="community">Community</h2>
<p><a href="https://www.conferencechats.org">Conference Chats</a>: We had our monthly Wednesday night meetup.</p>
<p><strong>Office Hours:</strong> I hosted Friday Office Hours.</p>
<h2 id="side-projects">Side projects</h2>
<p><strong><a href="https://django-news.com">Django News Newsletter</a>:</strong> We shipped issue <a href="https://django-news.com/issues/232#start">#232</a>.</p>
<p><a href="https://djangopackages.org">Django Packages</a>: I mostly worked on some notes and task ideas for Djangonaut Space.</p>
<p><a href="https://upgradedjango.com">Upgrade Django</a>: We had four updates this week, which is rare.</p>
<h2 id="writing">Writing</h2>
<p>I wrote and published every day. I started writing and publishing on my <a href="https://micro.webology.dev">https://micro.webology.dev</a> every day since February 1st, and this week marked my 100th day in a row of writing.</p>
<p><a href="https://micro.webology.dev/2024/05/11/on-vacation-autoresponders.html">2024-05-11</a>: <strong>๐ญ On vacation autoresponders and deleting email</strong></p>
<p><a href="https://micro.webology.dev/2024/05/10/til-buildandinspectpythonpackage-github.html">2024-05-10</a>: <strong>๐ TIL build-and-inspect-python-package GitHub Action workflow plus some bonus Nox + Tox</strong></p>
<p><a href="https://micro.webology.dev/2024/05/09/african-tech-book.html">2024-05-09</a>: <strong>๐ African tech book drive at PyCon US</strong></p>
<p><a href="https://micro.webology.dev/2024/05/08/office-hours-on.html">2024-05-08</a>: <strong>๐ฅ Office Hours on May 10th</strong></p>
<p><a href="https://micro.webology.dev/2024/05/07/223140.html">2024-05-07</a>: <strong>๐ธ Jason Isbell and the 400 Unit at Liberty Hall</strong></p>
<p><a href="https://micro.webology.dev/2024/05/06/205515.html">2024-05-06</a>: <strong>๐ช๏ธ Tornado Watch tonight</strong></p>
<h2 id="me-time">“Me” time</h2>
<p>Early Saturday morning, I wrote a podcast downloader script to make downloading podcast episodes easier. I like to run some of my favorite podcast episodes through <a href="https://goodsnooze.gumroad.com/l/macwhisper">MacWhisper</a> to generate a transcript to take better notes.</p>
<p>Saturday evening, I wired up <a href="https://www.fabfile.org">Fabric</a> to connect to my Macs to update them and run various <code>Just</code> recipes to keep everything updated. I ran into a few issues with my <code>.bashrc</code> file, but it was a good enough MVP.</p>
<h2 id="entertainment">Entertainment</h2>
<p>๐บ <a href="https://trakt.tv/movies/space-jam-a-new-legacy-2021">Space Jam: A New Legacy</a>: My oldest has gotten into basketball and this was a fun family movie.</p>
<p>๐บ <a href="https://trakt.tv/shows/dead-boy-detectives">Dead Boy Detectives</a>: I didn’t have high expectations for this series, but watching two ghosts solve mysteries was fun.</p>
<p>๐ฎ <a href="https://www.backloggd.com/games/teenage-mutant-ninja-turtles-shredders-revenge/">Teenage Mutant Ninja Turtles: Shredder’s Revenge</a>: We hadn’t played this one in a while, so we primarily focused on leveling up the two new characters from the expansion.</p>
<h2 id="next-week">Next week</h2>
<p>I leave for PyCon US on Wednesday and have a small mountain of things to finish between now and then. I finished my laundry tonight, so I can leisurely pack over the next few days.</p>
๐ TIL build-and-inspect-python-package GitHub Action workflow plus some bonus Nox + Tox
https://micro.webology.dev/2024/05/10/195921/
Fri, 10 May 2024 19:59:21 -0600http://webology.micro.blog/2024/05/10/195921/<p>TIL: via <a href="https://mastodon.social/@[email protected]/112419234883043196">@joshthomas</a> via <a href="https://mastodon.social/@treyhunner/112419178337248517">@treyhunner</a> via <a href="https://mastodon.social/@hynek/112418278282018728">@hynek</a> about the <a href="https://github.com/hynek/build-and-inspect-python-package">hynek/build-and-inspect-python-package</a> GitHub Action.ย </p>
<p>This workflow makes it possible for GitHub Actions to read your Python version classifiers to build a matrix or, as Trey put it, “<a href="https://github.com/treyhunner/countdown-cli/pull/208/commits/d33716b471a028ee1c56e35672e94e896dc5f360">Remove so much junk</a>” which is a pretty good example.ย </p>
<p>As a bonus, check out Hynek’s video on <a href="https://www.youtube.com/watch?v=ImBvrDvK-1U">NOXย vsย TOX โ WHAT are they for & HOW do you CHOOSE? ๐</a>ย </p>
<p><a href="https://www.youtube.com/watch?v=ImBvrDvK-1U">https://www.youtube.com/watch?v=ImBvrDvK-1U</a></p>
<p>Both <a href="https://nox.thea.codes">Nox</a> and <a href="https://tox.wiki">Tox</a> are great tools that automate testing in multiple Python environments.ย </p>
<p>I prefer Nox because it uses Python to write configs, which fits my brain better. I used Tox for over a decade, and there are some tox.ini files that I dread updating because I can only remember how I got here after a few hours of tinkering. That’s not Tox’s fault. I think that’s just a limitation of <code>ini</code> files and the frustration that comes from being unable to use Python when you have a complex matrix to try and sort out.ย </p>
<p>I recommend tryingย them out andย usingย the best tool for your brain.ย There isย noย wrongย path here.</p>
<p>PS: Thank you, Josh, for bringing this to my attention.</p>
๐ Everyone struggles with Django's static files
https://micro.webology.dev/2024/04/30/everyone-struggles-with-djangos-static/
Tue, 30 Apr 2024 21:07:44 -0600http://webology.micro.blog/2024/04/30/everyone-struggles-with-djangos-static/<p>Josh Thomas did a great job documenting and walking us through how he prefers to set up static files in his Django projects last week in his <a href="https://joshthomas.dev/blog/2024/how-i-organize-staticfiles-in-my-django-projects/">How I organize <code>staticfiles</code> in my Django projects</a> article.</p>
<p>Josh recommends the following config and naming convention:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># settings.py</span>
<span style="color:#75715e"># django.contrib.staticfiles</span>
STATIC_ROOT <span style="color:#f92672">=</span> BASE_DIR <span style="color:#f92672">/</span> <span style="color:#e6db74">"staticfiles"</span>
STATIC_URL <span style="color:#f92672">=</span> <span style="color:#e6db74">"/static/"</span>
STATICFILES_DIRS <span style="color:#f92672">=</span> [
BASE_DIR <span style="color:#f92672">/</span> <span style="color:#e6db74">"static"</span> <span style="color:#f92672">/</span> <span style="color:#e6db74">"dist"</span>,
BASE_DIR <span style="color:#f92672">/</span> <span style="color:#e6db74">"static"</span> <span style="color:#f92672">/</span> <span style="color:#e6db74">"public"</span>,
]
</code></pre></div><p>Overall, this is very similar to what I do, but I settled into calling my <code>STATICFILES_DIRS</code> folder <code>assets</code> or <code>frontend</code>. After seeing Josh’s example, I changed this value to <code>staticfiles</code> to match the setting variable more closely.</p>
<h2 id="updated-config">Updated config</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># settings.py</span>
<span style="color:#75715e"># django.contrib.staticfiles</span>
<span style="color:#75715e"># INPUT: Where to look for static files</span>
STATICFILES_DIRS <span style="color:#f92672">=</span> [
BASE_DIR <span style="color:#f92672">/</span> <span style="color:#e6db74">"staticfiles"</span> <span style="color:#f92672">/</span> <span style="color:#e6db74">"dist"</span>,
BASE_DIR <span style="color:#f92672">/</span> <span style="color:#e6db74">"staticfiles"</span> <span style="color:#f92672">/</span> <span style="color:#e6db74">"public"</span>,
]
<span style="color:#75715e"># OUTPUT: Where to put and look for static files to serve</span>
STATIC_ROOT <span style="color:#f92672">=</span> BASE_DIR <span style="color:#f92672">/</span> <span style="color:#e6db74">"static"</span>
<span style="color:#75715e"># SERVE: Where to serve static files</span>
STATIC_URL <span style="color:#f92672">=</span> <span style="color:#e6db74">"/static/"</span>
</code></pre></div><p>This also changes our <code>.gitignore</code> to match our new settings. Since all of our files will be collected by Django and placed into the <code>static</code> folder, we can tell git to ignore this folder.</p>
<p>We can also ignore the <code>staticfiles/dist/</code> folder if we have an asset building pipeline and need a place to store the intermediate files.</p>
<pre tabindex="0"><code class="language-unixconfig" data-lang="unixconfig">#.gitignore
static/
staticfiles/dist/
# Optionally, to universally ignore all 'dist' directories:
# dist
</code></pre>๐ธ Alphabet has never been more profitable
https://micro.webology.dev/2024/04/29/alphabet-has-never-been-more/
Mon, 29 Apr 2024 11:48:27 -0600http://webology.micro.blog/2024/04/29/alphabet-has-never-been-more/<p>This <a href="https://news.ycombinator.com/item?id=40176338">Hacker News comment</a> is quite the deep cut about <a href="https://in.mashable.com/tech/74212/sundar-pichai-led-google-layoffs-entire-python-team-and-offshores-work-to-munich-programmers-are-wor">Google’s recent layoff/firing of the entire Python Foundation team</a>.ย </p>
<p>This happened the same day Alphabet reported <a href="https://www.cnbc.com/2024/04/25/alphabet-set-to-report-first-quarter-results-after-market-close.html">$80.54 billion in revenue</a>.ย ย </p>
<blockquote>
<p>ย inย addition to contributing to upstreamย python, we</p>
</blockquote>
<blockquote>
<ul>
<li>maintained a stable version ofย pythonย withinย google,ย andย made sure thatย everything in the monorepo worked with it.ย inย my time on theย teamย we moved from 2.7 to 3.6, then incrementally to 3.11, each update taking months to over a year because the rule atย googleย is if you check any code in, you are responsible for every single breakage it causes</li>
</ul>
</blockquote>
<blockquote>
<p>ย </p>
</blockquote>
<blockquote>
<ul>
<li>maintained tools to keep thousands ofย third partyย packages constantly updated from theirย open sourceย versions, with patch queues for the ones that neededย google-specificย changes</li>
</ul>
</blockquote>
<blockquote>
<p>ย </p>
</blockquote>
<blockquote>
<ul>
<li>had highly customised versions of tools likeย pylintย and black, targeted toย google’sย style guide and overall codebase</li>
</ul>
</blockquote>
<blockquote>
<p>ย </p>
</blockquote>
<blockquote>
<ul>
<li>contributed to pybind11, and maintained tools for c++ integration</li>
</ul>
</blockquote>
<blockquote>
<p>ย </p>
</blockquote>
<blockquote>
<ul>
<li>developed and maintained build system rules forย python, includingย a largeย effort to moveย pythonย rules to pureย starlarkย code rather than having them entangled in the blaze/bazel core engine</li>
</ul>
</blockquote>
<blockquote>
<p>ย </p>
</blockquote>
<blockquote>
<ul>
<li>developed and maintained aย typecheckerย (pytype) that would do inference on code without typeย annotations,ย and work overย very largeย projects with a one-file-at-a-time architecture (this was my primary job atย google, ama)</li>
</ul>
</blockquote>
<blockquote>
<p>ย </p>
</blockquote>
<blockquote>
<ul>
<li>performed automated refactorings across hundreds of millions of lines of code</li>
</ul>
</blockquote>
<blockquote>
<p>ย </p>
</blockquote>
<blockquote>
<p>ย andย thatย was just the dev portion of our jobs.ย weย also acted as a help deskย of sortsย forย pythonย users atย google, helping troubleshoot trickyย issues,ย and point newcomers in the right direction.ย plusย weย worked with a lot of other teams, including the machine learning and AI teams, the colaboratory and IDE teams, teams likeย protobufย that integrated with and generatedย pythonย bindings, teams likeย google cloudย who wanted to offerย pythonย runtimes to their customers, teams like youtube who hadย an unusually largeย system built in python and needed to do extraordinary things to keep it performant and maintainable.</p>
</blockquote>
<blockquote>
<p>ย </p>
</blockquote>
<blockquote>
<p>ย andย weย did all this for years with fewer than 10 people, most of whom loved the work and the team so much that weย justย stayed on it for years.ย also, despite the understaffing, we had managers who were extremely good about maintaining work/life balance and the “marathon, not sprint” approach to work. asย iย said in another comment, it’s the best jobย i’veย ever had, andย i’llย miss it deeply.</p>
</blockquote>
<p>They have never been more profitable, made more money, or cared less about their employees and giving back to the communities that got them there.ย If I had worked at Google, I would not have been comfortable continuing to work there.ย </p>
<p>I plan to keep de-Googling, which is going to hurt. Several friends shared why they can’t cut ties with their products. I get it, but that is a sign of an unhealthy monopoly.ย </p>
<p>I pay for YouTube TV and YouTube Premium as everyone else does, but I’m starting with the business tools. I’d rather pay Zoom than use Google Video. I stopped using Chrome.ย </p>
<p>Gmail is theย hardestย service to separate from because it’s also an identity service. I’m using it less and less until it’s only an identity service. I have >2k websites registered with my Gmail address, which could take years to clean up. I don’t need to pay for extra storage space, and I’m wondering if Google Docs is doing anything for me to pay for.ย </p>
<p>So, while it will be harder to stop using Google Products than it should be, the first step is admitting the problem and then working on it. Friends, we all have work to do.</p>
๐ DjangoCon US Talks I'd Like to See 2024 Edition
https://micro.webology.dev/2024/04/28/djangocon-us-talks-id-like/
Sun, 28 Apr 2024 18:45:13 -0600http://webology.micro.blog/2024/04/28/djangocon-us-talks-id-like/<p>Continuing in my tradition of <a href="https://jefftriplett.com/2023/djangocon-us-talks-i-d-like-to-see-2023-edition/">2023</a>, <a href="https://jefftriplett.com/2022/djangocon-us-talks-i-d-like-to-see-2022-edition/">2022</a>, <a href="https://jefftriplett.com/2018/djangocon-us-talks-i-d-like-to-see-2018-edition/">2018</a>, <a href="https://jefftriplett.com/2017/django-talks-id-like-to-see/">2017</a>, and <a href="https://jefftriplett.com/2016/djangocon-us-talks-id-like-to-see/">2016</a> “DjangoCon US Talks I’d like to see” annual posts, here is my update for 2024.</p>
<p>This year’s <em>updated</em> talk and tutorial deadline is <a href="https://time.is/1200PM_29_Apr_2024_in_New_York?DjangoCon_US_2024_CFP_closes">April 29, 2024, at 12 PM EDT</a>, but check the official <a href="https://2024.djangocon.us/speaking/">speaking page</a> for updates and <a href="https://pretalx.com/djangocon-us-2024/cfp">submit your proposal.</a></p>
<p>Before you read mine, I suggest you check out <a href="https://katherinemichel.github.io/portfolio/djangocon-us-2024-topics-inspiration-list.html">Kati Michel’s</a> and <a href="https://winstel.dev/2024/03/01/talk-ideas-for-dcus-24/">Drew Winstel’s</a> list of talk ideas too.</p>
<h2 id="modern-django">Modern Django</h2>
<p>Three or four years ago, our Django stack was optimized to leverage the Django ORM to tie together a mix of authentication and REST API using the Django REST Framework. The front end was entirely buried by JavaScript, which printed HTML into your browser, and the HTML was entirely hidden away. What used to take a developer hours and days now took weeks because there were so many layers and abstractions to wade through.</p>
<p>Then it all started to change a few years ago, and we learned that we don’t need the additional weeks of development time to use JS/JSON layers for which we may never build that iOS/Android app. We don’t need React for contact us forms, and 90% of our web tasks could be built in a day.</p>
<p>The theme I’d like to see more of this year is doing more with HTML instead of hiding it behind layers of JavaScript.</p>
<p>I want to see lots of talks about technologies like HTMx and frameworks like Tailwind CSS, which are firmly centered inside HTML, instead of trying to avoid using HTML.</p>
<h2 id="the-two-non-python-languages-we-are-starting-to-talk-about">The two non-Python languages we are starting to talk about</h2>
<p>Rust and WASM are starting to change everything. How do they fit in with Django and Python?</p>
<h2 id="theme-rust">Theme: Rust</h2>
<p>The Rust language is shaping the landscape of Python productivity in ways that none of us could have predicted. Tools written in Rust are significantly faster and are gaining steam.</p>
<ul>
<li>What can a Django developer take from Rust?</li>
<li>How do I develop a package for Django and Python that uses Rust?</li>
<li>How libraries like PyDantic are speeding up development (Django Ninja uses it)</li>
</ul>
<h2 id="always-nice-to-have-talks">Always nice to have talks</h2>
<ul>
<li>What’s new in Django 5.x?</li>
<li>What’s new in Python 3.13?</li>
<li>What’s new in Wagtail CMS and Django CMS?</li>
<li>Where do we want the DSF to go?</li>
<li>How to contribute to Django or third party projects</li>
</ul>
<h2 id="embracing-html-and-lightweight-front-end-frameworks">Embracing HTML and lightweight front-end frameworks</h2>
<p>Last year, I predicted it would be the year of “you probably don’t need JavaScript,” and I still feel good about that. We saw <a href="https://htmx.org">HTMx</a> take off, but I think Django is still underserved and could benefit from more talks:</p>
<ul>
<li>Django 5 added more granular template/form styling , and we could use a talk that deep dives</li>
<li>Using Django forms with htmx</li>
<li>Using search with htmx</li>
<li>Whatever cool things you are doing that I didn’t mention here</li>
</ul>
<h2 id="panel-ideas">Panel ideas</h2>
<ul>
<li>Django Fellows panel</li>
<li>Django content creators' panel</li>
<li>Django apps and community - It would be nice to hear from the more significant apps and community projects.</li>
</ul>
<h2 id="wagtail-cms">Wagtail CMS</h2>
<ul>
<li>How do I migrate from WordPress to Wagtail CMS?</li>
<li>How to properly test with Wagtail CMS for me, because I struggle with it</li>
</ul>
<h2 id="talks-i-dont-want-to-see">Talks I don’t want to see</h2>
<p>I find myself at a weird crossroads because I have never discouraged certain topics, but we need to talk more openly about what is bad for our industry and the world instead of avoiding it. Otherwise, we are left with dead air, and people assume we are all on the same page.</p>
<p>To kick us off, there are talks and topics that I do not want to see:</p>
<ul>
<li>BAD AI talks and ones that spread more FUD into the community</li>
<li>crypto talks</li>
<li>why technology sucks</li>
<li>proposing a talk because you dislike or even hate something</li>
<li>bashing other communities</li>
<li>bashing our communities</li>
<li>lightning talks disguised as questions (not so much a talk idea, but this has been on my mind for a while)</li>
</ul>
<p>So please, <strong>none</strong> of these:</p>
<ul>
<li>The future of Django depends on AI</li>
<li>Why tech needs AI</li>
<li>Crypto talks</li>
<li>Your product unless you are sponsoring.</li>
</ul>
<h2 id="good-luck">Good luck</h2>
<p>You have less than a day, but between my list, <a href="https://katherinemichel.github.io/portfolio/djangocon-us-2024-topics-inspiration-list.html">Kati’s list</a>, and <a href="https://winstel.dev/2024/03/01/talk-ideas-for-dcus-24/">Drew’s list</a> you have no excuses for saying you didn’t know what to talk about at DjangoCon US this year.</p>
<p>Good luck!</p>
๐ค Super Bot Fight ๐ฅ
https://micro.webology.dev/2024/04/18/super-bot-fight/
Thu, 18 Apr 2024 22:41:03 -0600http://webology.micro.blog/2024/04/18/super-bot-fight/<p>In March, I wrote about my robots.txt research and how I started proactively and defensively blocking AI Agents in my <a href="https://micro.webology.dev/2024/03/20/on-robotstxt.html">๐ค On Robots.txt</a>. Since March, I have updated my Django projects to add more robots.txt rules.</p>
<p>Earlier this week, I ran across this <a href="https://ethanmarcotte.com/wrote/blockin-bots/">Blockinโ bots.</a> blog post and this example, the <code>mod_rewrite</code> rule blocks AI Agents via their User-Agent strings.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-apacheconf" data-lang="apacheconf"><span style="color:#f92672"><IfModule</span> <span style="color:#e6db74">mod_rewrite.c</span><span style="color:#f92672">></span>
RewriteEngine <span style="color:#66d9ef">on</span>
RewriteBase /
<span style="color:#75715e"># block โAIโ bots</span>
RewriteCond %{HTTP_USER_AGENT} (AdsBot-Google|Amazonbot|anthropic-ai|Applebot|AwarioRssBot|AwarioSmartBot|Bytespider|CCBot|ChatGPT|ChatGPT-User|Claude-Web|ClaudeBot|cohere-ai|DataForSeoBot|Diffbot|FacebookBot|FacebookBot|Google-Extended|GPTBot|ImagesiftBot|magpie-crawler|omgili|Omgilibot|peer39_crawler|PerplexityBot|YouBot) [NC]
RewriteRule ^ โ [F]
<span style="color:#f92672"></IfModule></span>
</code></pre></div><p>Since none of my projects use Apache, and I was short on time, I decided to leave this war to the bots.</p>
<h2 id="django-middleware">Django Middleware</h2>
<p>I asked ChatGPT to convert this snippet to a piece of Django Middleware called Super Bot Fight. After all, if we don’t have time to keep up with bots, then we could leverage this technology to help fight against them.</p>
<p>In theory, this snippet passed my eyeball test and was good enough:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># middleware.py</span>
<span style="color:#f92672">from</span> django.http <span style="color:#f92672">import</span> HttpResponseForbidden
<span style="color:#75715e"># List of user agents to block</span>
BLOCKED_USER_AGENTS <span style="color:#f92672">=</span> [
<span style="color:#e6db74">"AdsBot-Google"</span>,
<span style="color:#e6db74">"Amazonbot"</span>,
<span style="color:#e6db74">"anthropic-ai"</span>,
<span style="color:#e6db74">"Applebot"</span>,
<span style="color:#e6db74">"AwarioRssBot"</span>,
<span style="color:#e6db74">"AwarioSmartBot"</span>,
<span style="color:#e6db74">"Bytespider"</span>,
<span style="color:#e6db74">"CCBot"</span>,
<span style="color:#e6db74">"ChatGPT"</span>,
<span style="color:#e6db74">"ChatGPT-User"</span>,
<span style="color:#e6db74">"Claude-Web"</span>,
<span style="color:#e6db74">"ClaudeBot"</span>,
<span style="color:#e6db74">"cohere-ai"</span>,
<span style="color:#e6db74">"DataForSeoBot"</span>,
<span style="color:#e6db74">"Diffbot"</span>,
<span style="color:#e6db74">"FacebookBot"</span>,
<span style="color:#e6db74">"Google-Extended"</span>,
<span style="color:#e6db74">"GPTBot"</span>,
<span style="color:#e6db74">"ImagesiftBot"</span>,
<span style="color:#e6db74">"magpie-crawler"</span>,
<span style="color:#e6db74">"omgili"</span>,
<span style="color:#e6db74">"Omgilibot"</span>,
<span style="color:#e6db74">"peer39_crawler"</span>,
<span style="color:#e6db74">"PerplexityBot"</span>,
<span style="color:#e6db74">"YouBot"</span>,
]
<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">BlockBotsMiddleware</span>:
<span style="color:#66d9ef">def</span> __init__(self, get_response):
self<span style="color:#f92672">.</span>get_response <span style="color:#f92672">=</span> get_response
<span style="color:#66d9ef">def</span> __call__(self, request):
<span style="color:#75715e"># Check the User-Agent against the blocked list</span>
user_agent <span style="color:#f92672">=</span> request<span style="color:#f92672">.</span>META<span style="color:#f92672">.</span>get(<span style="color:#e6db74">"HTTP_USER_AGENT"</span>, <span style="color:#e6db74">""</span>)
<span style="color:#66d9ef">if</span> any(bot <span style="color:#f92672">in</span> user_agent <span style="color:#66d9ef">for</span> bot <span style="color:#f92672">in</span> BLOCKED_USER_AGENTS):
<span style="color:#66d9ef">return</span> HttpResponseForbidden(<span style="color:#e6db74">"Access denied"</span>)
response <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>get_response(request)
<span style="color:#66d9ef">return</span> response
</code></pre></div><p>To use this middleware, you would update your Django <code>settings.py</code> to add it to your <code>MIDDLEWARE</code> setting.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># settings.py</span>
MIDDLEWARE <span style="color:#f92672">=</span> [
<span style="color:#f92672">...</span>
<span style="color:#e6db74">"middleware.BlockBotsMiddleware"</span>,
<span style="color:#f92672">...</span>
]
</code></pre></div><h2 id="tests">Tests?</h2>
<p>If this middleware works for you and you care about testing, then these tests should also work:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python">
<span style="color:#f92672">import</span> pytest
<span style="color:#f92672">from</span> django.http <span style="color:#f92672">import</span> HttpRequest
<span style="color:#f92672">from</span> django.test <span style="color:#f92672">import</span> RequestFactory
<span style="color:#f92672">from</span> middleware <span style="color:#f92672">import</span> BlockBotsMiddleware
<span style="color:#a6e22e">@pytest</span><span style="color:#f92672">.</span>mark<span style="color:#f92672">.</span>parametrize(<span style="color:#e6db74">"user_agent, should_block"</span>, [
(<span style="color:#e6db74">"AdsBot-Google"</span>, <span style="color:#66d9ef">True</span>),
(<span style="color:#e6db74">"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"</span>, <span style="color:#66d9ef">False</span>),
(<span style="color:#e6db74">"ChatGPT-User"</span>, <span style="color:#66d9ef">True</span>),
(<span style="color:#e6db74">"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"</span>, <span style="color:#66d9ef">False</span>),
])
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">test_user_agent_blocking</span>(user_agent, should_block):
<span style="color:#75715e"># Create a request factory to generate request instances</span>
factory <span style="color:#f92672">=</span> RequestFactory()
request <span style="color:#f92672">=</span> factory<span style="color:#f92672">.</span>get(<span style="color:#e6db74">'/'</span>, HTTP_USER_AGENT<span style="color:#f92672">=</span>user_agent)
<span style="color:#75715e"># Middleware setup</span>
middleware <span style="color:#f92672">=</span> BlockBotsMiddleware(get_response<span style="color:#f92672">=</span><span style="color:#66d9ef">lambda</span> request: HttpResponse())
response <span style="color:#f92672">=</span> middleware(request)
<span style="color:#75715e"># Check if the response should be blocked or allowed</span>
<span style="color:#66d9ef">if</span> should_block:
<span style="color:#66d9ef">assert</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> <span style="color:#ae81ff">403</span>, <span style="color:#e6db74">f</span><span style="color:#e6db74">"Request with user agent '</span><span style="color:#e6db74">{</span>user_agent<span style="color:#e6db74">}</span><span style="color:#e6db74">' should be blocked."</span>
<span style="color:#66d9ef">else</span>:
<span style="color:#66d9ef">assert</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">!=</span> <span style="color:#ae81ff">403</span>, <span style="color:#e6db74">f</span><span style="color:#e6db74">"Request with user agent '</span><span style="color:#e6db74">{</span>user_agent<span style="color:#e6db74">}</span><span style="color:#e6db74">' should not be blocked."</span>
</code></pre></div><h2 id="enhancements">Enhancements</h2>
<p>To use this code in production, I would normalize the <code>user_agent</code> and <code>BLOCKED_USER_AGENTS</code> variables to be case-insensitive.</p>
<p>I would also consider storing my list of user agents in a Django model or using a project like <a href="https://github.com/jazzband/django-robots">django-robots</a> instead of a hard-coded Python list.</p>
๐ Starship is the shell prompt you have been looking for ๐
https://micro.webology.dev/2024/04/17/starship-is-the-shell-prompt/
Wed, 17 Apr 2024 20:37:05 -0600http://webology.micro.blog/2024/04/17/starship-is-the-shell-prompt/<p>I spent a lot of time in my shell (terminal, iTerm, bash, whatever you prefer), and by and large, my least favorite and most frustrating chore has always been trying to customize my shell prompt.</p>
<p>A few years ago, I tried to switch from BASH to Zsh and even the Fish Shell, and I quickly reverted because I found customizing my prompt to be such a chore.</p>
<p><a href="https://starship.rs">Starship</a> is one of those few life-changing development utilities that quickly solved how to customize my shell prompt. Starship also works across various shells, which means changing shells doesn’t mean starting over with a new prompt. Even though Fish and Zsh never stuck for me, Starship was a keeper.</p>
<p>Starship’s default prompt is really good and is a nicer starting point than Zsh or BASH gives you by default. Starship includes a bunch of default settings, which enables support for tools like aws, battery percentage, bun, git, k8s, node js, python, etc, so most of <a href="https://github.com/jefftriplett/dotfiles/blob/main/home/.config/starship.toml">my dotfiles config</a> is turning off the features that I don’t care about.</p>
<p>There is a <a href="https://draculatheme.com/starship">Dracula Theme for Starship</a>, which is my go-to theme for any application that I can find that supports the color scheme.</p>
<p>Setting up and configuring Starship is a breeze. The documentation is clear and concise, and Starship’s settings are neatly organized in one readable and updated file, <code>~/.config/starship.toml</code>. Best of all, any changes you make are immediately applied, eliminating the need to restart your shell session. With Starship, you can confidently customize your prompt and see the results instantly.</p>
<p>If you use Homebrew, <code>brew install starship</code> will install it. You can activate Starship by adding <code>eval "$(starship init bash)"</code> to your <code>~/.bashrc</code> file.</p>
<p>If you have been thinking about trying out Starship or need help figuring out where to start or how to customize your shell prompt, I highly recommend it.</p>
๐ Refactoring and fiddling with Django migrations for pending pull requests ๐
https://micro.webology.dev/2024/04/06/refactoring-and-fiddling-with-django/
Sat, 06 Apr 2024 13:59:54 -0600http://webology.micro.blog/2024/04/06/refactoring-and-fiddling-with-django/<p>One of Django’s most powerful features is the ORM, which includes a robust migration framework. One of Django’s most misunderstood features is Django migrations because it just works 99% of the time.</p>
<p>Even when working solo, Django migrations are highly reliable, working 99.9% of the time and offering better uptime than most web services you may have used last week.</p>
<p>The most common stumbling block for developers of all skill levels is rolling back a Django migration and prepping a pull request for review.</p>
<p>I’m not picky about pull requests or git commit history because I default to using the “Squash and merge” feature to turn all pull request commits into one merge commit. The merge commit tells me when, what, and why something changed if I need extra context.</p>
<p>I am pickier about seeing >2 database migrations for any app unless a data migration is involved. It’s common to see 4 to 20 migrations when someone works on a database feature for a week. Most of the changes tend to be fiddly, where someone adds a field, renames the field, renames it again, and then starts using it, which prompts another <code>null=True</code> change followed by a <code>blank=True</code> migration.</p>
<p>For small databases, none of this matters.</p>
<p>For a database with 10s or 100s of millions of records, these small changes can cause minutes of downtime <em>per</em> migration, which amounts to a throwaway change. While there are ways to mitigate most migration downtime situations, that’s different from my point today.</p>
<p>I’m also guilty of being fiddly with my Django model changes because I know I can delete and refactor them before requesting approval. The process I use is probably worth sharing because once every new client comes up.</p>
<p>Let’s assume I am working on <a href="https://jobs.django-news.com">Django News Jobs</a>, and I am looking over my pull request one last time before I ask someone to review it. That’s when I noticed four migrations that could quickly be rebuilt into one, starting with my <code>0020*</code> migration in my <code>jobs</code> app.</p>
<p>The rough steps that I would do are:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash"><span style="color:#75715e"># step 1: see the state of our migrations</span>
$ python -m manage showmigrations jobs
jobs
<span style="color:#f92672">[</span>X<span style="color:#f92672">]</span> 0001_initial
...
<span style="color:#f92672">[</span>X<span style="color:#f92672">]</span> 0019_alter_iowa_versus_unconn
<span style="color:#f92672">[</span>X<span style="color:#f92672">]</span> 0020_alter_something_i_should_delete
<span style="color:#f92672">[</span>X<span style="color:#f92672">]</span> 0021_alter_uconn_didnt_foul
<span style="color:#f92672">[</span>X<span style="color:#f92672">]</span> 0022_alter_nevermind_uconn_cant_rebound
<span style="color:#f92672">[</span>X<span style="color:#f92672">]</span> 0023_alter_iowa_beats_uconn
<span style="color:#f92672">[</span>X<span style="color:#f92672">]</span> 0024_alter_south_carolina_sunday_by_four
<span style="color:#75715e"># step 2: rollback migrations to our last "good" state</span>
$ python -m manage migrate jobs <span style="color:#ae81ff">0019</span>
<span style="color:#75715e"># step 3: delete our new migrations</span>
$ rm jobs/migrations/002*
<span style="color:#75715e"># step 4: rebuild migrations </span>
python -m manage makemigrations jobs
<span style="color:#75715e"># step 5: profit </span>
python -m manage migrate jobs
</code></pre></div><p>95% of the time, this is all I ever need to do.</p>
<p>Occasionally, I check out another branch with conflicting migrations, and I’ll get my <strong>local</strong> database in a weird state.</p>
<p>In those cases, check out the <code>--fake</code> (“Mark migrations as run without actually running them.") and <code>--prune</code> (“Delete nonexistent migrations from the <code>django_migrations</code> table.") options. The fake and prune operations saved me several times when my <code>django_migrations</code> table was out of sync, and I knew that SQL tables were already altered.</p>
<h2 id="what-not-squashmigrations">What not <code>squashmigrations</code>?</h2>
<p>Excellent question. <a href="https://docs.djangoproject.com/en/5.0/topics/migrations/#migration-squashing">Squashing migrations</a> is wonderful if you care about keeping every or most of the operations each migration is doing. Most of the time, I do not, so I overlook it.</p>
โณ Syncing Django Waffle feature flags
https://micro.webology.dev/2024/04/05/syncing-django-waffle-feature-flags/
Fri, 05 Apr 2024 20:58:28 -0600http://webology.micro.blog/2024/04/05/syncing-django-waffle-feature-flags/<p>The <a href="https://github.com/jazzband/django-waffle">django-waffle</a> feature flag library is helpful for projects where we want to release and test new features in production and have a controlled rollout. I also like using feature flags for resource-intensive features on a website that we want to toggle off during high-traffic periods. It’s a nice escape hatch to fall back on if we need to turn off a feature and roll out a fix without taking down your website.</p>
<p>While Waffle is a powerful tool, I understand the challenge of keeping track of feature flags in both code and the database. It’s a pain point that many of us have experienced.</p>
<p>Waffle has a <code>WAFFLE_CREATE_MISSING_FLAGS=True</code> setting that we can use to tell Waffle to create any missing flags in the database should it find one. While this helps discover which flags our application is using, we need to figure out how to clean up old flags in the long term.</p>
<p>The pattern I landed on combines storing all our known feature flags and a note about what they do in our main settings file.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># settings.py</span>
<span style="color:#f92672">...</span>
WAFFLE_CREATE_MISSING_FLAGS<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
WAFFLE_FEATURE_FLAGS <span style="color:#f92672">=</span> {
<span style="color:#e6db74">"flag_one"</span>: <span style="color:#e6db74">"This is a note about flag_one"</span>,
<span style="color:#e6db74">"flag_two"</span>: <span style="color:#e6db74">"This is a note about flag_two"</span>,
}
</code></pre></div><p>We will use a management command to sync every feature flag we have listed in our settings file, and then we will clean up any missing feature flags.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># management/commands/sync_feature_flags.py</span>
<span style="color:#f92672">import</span> djclick <span style="color:#66d9ef">as</span> click
<span style="color:#f92672">from</span> django.conf <span style="color:#f92672">import</span> settings
<span style="color:#f92672">from</span> waffle.models <span style="color:#f92672">import</span> Flag
<span style="color:#a6e22e">@click</span>()
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">command</span>():
<span style="color:#75715e"># Create flags that don't exist</span>
<span style="color:#66d9ef">for</span> name, note <span style="color:#f92672">in</span> settings<span style="color:#f92672">.</span>WAFFLE_FEATURE_FLAGS<span style="color:#f92672">.</span>items():
flag, created <span style="color:#f92672">=</span> Flag<span style="color:#f92672">.</span>objects<span style="color:#f92672">.</span>update_or_create(
name<span style="color:#f92672">=</span>name, defaults<span style="color:#f92672">=</span>{<span style="color:#e6db74">"note"</span>: note}
)
<span style="color:#66d9ef">if</span> created:
print(<span style="color:#e6db74">f</span><span style="color:#e6db74">"Created flag </span><span style="color:#e6db74">{</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74"> (</span><span style="color:#e6db74">{</span>flag<span style="color:#f92672">.</span>pk<span style="color:#e6db74">}</span><span style="color:#e6db74">)"</span>)
<span style="color:#75715e"># Delete flags that are no longer registered in settings</span>
<span style="color:#66d9ef">for</span> flag <span style="color:#f92672">in</span> Flag<span style="color:#f92672">.</span>objects<span style="color:#f92672">.</span>exclude(name__in<span style="color:#f92672">=</span>settings<span style="color:#f92672">.</span>FEATURE_FLAGS<span style="color:#f92672">.</span>keys()):
flag<span style="color:#f92672">.</span>delete()
print(<span style="color:#e6db74">f</span><span style="color:#e6db74">"Deleted flag </span><span style="color:#e6db74">{</span>flag<span style="color:#f92672">.</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74"> (</span><span style="color:#e6db74">{</span>flag<span style="color:#f92672">.</span>pk<span style="color:#e6db74">}</span><span style="color:#e6db74">)"</span>)
</code></pre></div><p>We can use the <code>WAFFLE_CREATE_MISSING_FLAGS</code> settings as a failsafe to create any flags we might have accidently missed. They will stick out because they will not have a note associated with them.</p>
<p>This pattern is also helpful in solving similar problems for scheduled tasks, which might also store their schedules in the database.</p>
<p>Check out this example in the <a href="https://github.com/HackSoftware/Django-Styleguide?tab=readme-ov-file#periodic-tasks">Django Styleguide</a> for how to sync Celery’s scheduled tasks.</p>
โฌ๏ธ The Upgrade Django project
https://micro.webology.dev/2024/04/04/the-upgrade-django-project/
Thu, 04 Apr 2024 18:37:35 -0600http://webology.micro.blog/2024/04/04/the-upgrade-django-project/<p><a href="https://upgradedjango.com">Upgrade Django</a> is a <a href="https://www.revsys.com">REVSYS</a> project we created six years ago and launched three years ago.</p>
<p>The goal of Upgrade Django was to create a resource that made it easy to see at a glance which versions of the <a href="djangoproject.com">Django web framework</a> are maintained and supported. We also wanted to catalog every release and common gotchas and link to helpful information like release notes, blog posts, and the tagged git branch on GitHub.</p>
<p>We also wanted to make it easier to tell how long a given version of <a href="https://upgradedjango.com/4.2/">Django would be supported</a> and what <a href="https://upgradedjango.com/4.1/">phase of its release cycle</a> it is in.</p>
<h2 id="future-features">Future features</h2>
<p>We have over a dozen features planned, but it’s a project that primarily serves its original purpose.</p>
<p>One feature on my list is that I’d love to see every backward incompatible change between two Django versions. This way, if someone knows their website is running on <a href="https://upgradedjango.com/3.2/">Django 3.2</a>, they could pick <a href="https://upgradedjango.com/4.2/">Django 4.2</a> or <a href="https://upgradedjango.com/5.0/">Django 5.0</a> version and get a comprehensive list with links to everything they need to upgrade between versions.</p>
<p>Projects like Upgrade Django are fun to work on because once you collect a bunch of data and start working with it, new ways of comparing and presenting the information become more apparent.</p>
<p>If you have ideas for improving Upgrade Django that would be useful to your needs, we’d love to hear about them.</p>
๐ Mise is really fast
https://micro.webology.dev/2024/04/03/mise-is-really-fast/
Wed, 03 Apr 2024 18:27:03 -0600http://webology.micro.blog/2024/04/03/mise-is-really-fast/<p>A month or two ago, I stumbled on <a href="https://github.com/epogrebnyak/justpath"><code>justpath</code></a>, which is worth installing to help clean up any PATH environment issues you may or may not realize you have.</p>
<p>I was frustrated because every time I created a new tab in iTerm2, it took three or more seconds for whatever slowed everything in my bash/dotfiles.</p>
<p>With <code>justpath</code>, I found it straightforward to identify and address several paths related to ruby, rbenv, cargo, golang, and others that were causing issues. Rather than continue to fight these apps, I deleted them and removed them from my <code>.bash*</code> files. To see if I broke anything, I opened a new iTerm tab, and I had a new shell prompt in less than a second.</p>
<p>Since I needed access to Ruby, NPM, and other tools, I switched over to <a href="https://github.com/jdx/mise"><code>mise</code></a>, which promises to be one tool for managing these languages and tools. <code>mise</code> is also written in Rust, so I know it’s fast by default.</p>
<p>To get started, check out Mise’s <a href="https://github.com/jdx/mise?tab=readme-ov-file#quickstart">Quickstart</a> docs in their README. I modified their bash example and added it to my <code>.bash*</code> files.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#66d9ef">if</span> command -v mise > /dev/null 2>&1; <span style="color:#66d9ef">then</span>
eval <span style="color:#e6db74">"</span><span style="color:#66d9ef">$(</span>mise activate bash<span style="color:#66d9ef">)</span><span style="color:#e6db74">"</span>;
<span style="color:#66d9ef">fi</span>
</code></pre></div><p>Once Mise was set up, and I opened a new tab, I installed Go, Node.js, Ruby, and Rust:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#75715e"># to install Go</span>
mise install golang
<span style="color:#75715e"># to install Node.js</span>
mise install node
<span style="color:#75715e"># to install Ruby</span>
mise install ruby
<span style="color:#75715e"># to install Rust</span>
mise install rust
<span style="color:#75715e"># to reshim/add these tool to our path...</span>
mise reshim
</code></pre></div><p>Eventually, I used Mise to install kubectl (boo) and yarn (hiss). Here is a list of everything on my older MacBook Pro that Mise is managing for me.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ mise list
Plugin Version Config Source Requested
go 1.22.2
kubectl 1.22.2
node 20.10.0 ~/.tool-versions 20.10.0
ruby 3.3.0
rust 1.77.1
yarn 1.22.19
</code></pre></div><p>Another nice feature of Mise is that you can list the versions of each tool that you want Mise to install and manage for you in a <code>.tool-versions</code> file.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ cat ~/.tool-versions
<span style="color:#75715e"># direnv latest</span>
<span style="color:#75715e"># golang latest</span>
<span style="color:#75715e"># kubectl 1.22.2</span>
<span style="color:#75715e"># nodejs 18.9.1</span>
<span style="color:#75715e"># nodejs lts</span>
<span style="color:#75715e"># ruby 3.1.2</span>
<span style="color:#75715e"># starship latest</span>
<span style="color:#75715e"># yarn 1.22.19</span>
nodejs 20.10.0
</code></pre></div><p>As a nice bonus, I discovered I could use Mise to manage the <code>direnv</code> and <code>starship</code> tools. In theory, Mise can also replace <code>direnv</code> with their built-in <a href="https://mise.jdx.dev/environments.html">environment</a> support), but I haven’t tried that feature yet.</p>
<h2 id="results">Results</h2>
<p>Mise is a nice win. I cut my new iTerm2 tab/session time down to close to 1/4 of a second, and I have more granular control over which exact versions of each tool I’m using.</p>
<p>Overall, I still prefer to use <a href="https://github.com/pyenv/pyenv"><code>pyenv</code></a> to manage my Python versions, so I’m keeping that for now mainly because I use <code>pyenv-virtualenvwrapper</code> and a few other nice-to-haves. See my <a href="https://micro.webology.dev/2024/02/10/python-development-on.html">Python Development on macOS Notes: pyenv and pyenv-virtualenvwrapper</a> article on why and how I manage those.</p>
๐ฅ An update on OpenSSF's XZ fumble
https://micro.webology.dev/2024/04/01/an-update-on-openssfs-xz/
Mon, 01 Apr 2024 21:07:39 -0600http://webology.micro.blog/2024/04/01/an-update-on-openssfs-xz/<h2 id="openssfs-scorecards-is-going-just-great-">OpenSSF’s Scorecards is Going Just Great ๐ฅ</h2>
<p>While I’m happy that OpenSSF updated their blog post to remove all mentions of their scorecards, they forgot to mention why, apologize, or publicly acknowledge that they messed up.</p>
<p>Since they rushed their first blog post and spent so little time on it, I wanted to point out the changes and equally spend very little time on why their scorecard is actively harmful to projects that handle security well.</p>
<p>Today was a rare day off for me, and documenting the numerous red flags of OpenSSF’s Scorecards isn’t how I spent my Monday night, nor will I spend my Tuesday or any night on this.</p>
<p>Instead, check out the Scorecards for these three popular projects.</p>
<ul>
<li><a href="https://securityscorecards.dev/viewer/?uri=github.com/pallets/flask">Flask</a> scores a 6.9</li>
<li><a href="https://securityscorecards.dev/viewer/?uri=github.com/python/cpython">cpython</a> scores a 7.0</li>
<li><a href="https://securityscorecards.dev/viewer/?uri=github.com/django/django">Django</a> scores a 7.2</li>
</ul>
<p>If you aren’t sure where to start, compare the “Pinned-Dependencies” and “Best-Practices” over the three projects. Then, look at the reference files and see if you agree.</p>
<p>๐คท</p>
<h2 id="updates">Updates</h2>
<p>This <a href="https://mastodon.social/@[email protected]/112198968259149635">post by David Lord</a> is worth a star.</p>
<iframe src="https://mas.to/@davidism/112198968229660761/embed" width="400" allowfullscreen="allowfullscreen" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms"></iframe>
Things I can never remember how to do: Django Signals edition
https://micro.webology.dev/2024/03/25/things-i-can-never-remember/
Mon, 25 Mar 2024 15:13:15 -0600http://webology.micro.blog/2024/03/25/things-i-can-never-remember/<p>I am several weeks into working on a project with my colleague, Lacey Henschel. Today, while reviewing one of her pull requests, I was reminded how to test a Django Signal via mocking.</p>
<p>Testing Django signals is valuable to me because I need help remembering how to test a signal, and even with lots of effort, it never works. So bookmark this one, friends. It works.</p>
<p>Thankfully, she wrote it up in one of her <a href="https://github.com/williln/til/blob/main/django/how_i_added_django_activity_stream_with_test.md">TIL: How I set upย <code>django-activity-stream</code>, including a simple test</a></p>
<iframe src="https://hachyderm.io/@lacey/112157634161607539/embed" width="400" allowfullscreen="allowfullscreen" sandbox="allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-forms"></iframe>
<p><a href="https://mastodon.social/@[email protected]">https://mastodon.social/@[email protected]</a></p>
2024-W12 - Weeknotes for Week 12: March 18 to March 24
https://micro.webology.dev/2024/03/24/w-weeknotes-for-week-march/
Sun, 24 Mar 2024 20:02:23 -0600http://webology.micro.blog/2024/03/24/w-weeknotes-for-week-march/<h2 id="family">Family</h2>
<p>Spring Break is over for my kids, and they both returned to school. Both kids seemed healthy again, but then we had a doctor’s appointment on Thursday and a follow-up appointment on Friday. Everything was fine, and everyone was healthy.</p>
<h2 id="work">Work</h2>
<p>It was a short week for us. I spent most of my week working on the green field project again and then on some internal projects.</p>
<h2 id="community">Community</h2>
<p>I only had a little time for community projects this week. What little time I did have was spent at the bank getting paperwork for the next DEFNA board.</p>
<h2 id="side-projects">Side projects</h2>
<p><strong>Django News Newsletter:</strong>ย We shipped issueย <a href="https://django-news.com/issues/224#start">#224</a>.</p>
<p><a href="https://jobs.django-news.com">Django News Jobs</a>: I fixed/simplified a few layout issues. I also spoke to a few companies that use the service, changed how jobs are submitted, and cleaned up some of the messaging around it.</p>
<p>Django News Portal: Django News has a portal I have never publicly launched. I use it for some of my contributions and to double-check that we get all the big releases. I spend some time working on it over my break.</p>
<p>Django News Browser Extension: We also have an unreleased Browser Extension I have used for three or more years. I used it with Chrome, but since switching to Vivaldi, I have yet to figure out how to make it work as my new page. I might release this in April.</p>
<p><strong>Django Packages:</strong>ย We only had a few minor updates this week. I fixed UV, and we improved Sentry profiling.</p>
<p>Fun projects: I dabbled with a Bootstrap to Tailwind CSS converter project. I also dusted off an old Git Bot project I wrote about in my <a href="https://micro.webology.dev/2024/03/23/on-scratching-itches.html">On Scratching itches with Python</a> post.</p>
<h2 id="writing">Writing</h2>
<p>I wrote and published every day.</p>
<p><a href="https://micro.webology.dev/2024/03/23/on-scratching-itches.html">2024-03-23</a>:ย <strong>On scratching itches with Python</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/22/automated-python-and.html">2024-03-22</a>:ย <strong>Automated Python and Django upgrades</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/21/fixing-a-css.html">2024-03-21</a>:ย <strong>Fixing a CSS layout issue</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/20/on-robotstxt.html">2024-03-20</a>:ย <strong>๐ค On Robots.txt</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/19/justfile-alfred-plugin.html">2024-03-19</a>:ย <strong>Justfile Alfred Plugin</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/18/weeknotes-for-week.html">2024-03-18</a>:ย <strong>Weeknotes for Week 11: March 11 to March 17</strong>ย </p>
<h2 id="me-time">“Me” time</h2>
<p>I took my annual March Madness break and watched basketball from Thursday through Sunday.</p>
<p>My Jayhawks Men’s basketball team won the first round but fell in the second round, while the Jayhawks Women’s team was still in their respective tournament. Overall, it was a rare down year for the men, and it was great to see the women make the tournament and knock off a good team to advance to the Round of 32.</p>
<h2 id="next-week">Next week</h2>
<p>I was asked to appear on two different podcasts, and I’ll be recording those next week.</p>
<p>As of tonight, I plan to host Office Hours next week, but someone mentioned it being a holiday weekend. I will figure that out sooner than later this week.</p>
On scratching itches with Python
https://micro.webology.dev/2024/03/23/on-scratching-itches-with-python/
Sat, 23 Mar 2024 22:26:41 -0600http://webology.micro.blog/2024/03/23/on-scratching-itches-with-python/<p>Python is such a fantastic glue language. Last night, while watching March Madness basketball games, I had a programming itch I wanted to scratch.</p>
<p>I dusted off a demo I wrote several years ago. It used Python’s subprocess module, which strings together a bunch of shell commands to perform a git checkout, run a few commands, and then commit the results. The script worked, but I struggled to get it fully working in a production environment.</p>
<p>To clean things up and as an excuse to try out a new third-party package, I converted the script to use:</p>
<ul>
<li>
<p><a href="https://github.com/gitpython-developers/GitPython">GitPython</a> - GitPython is a Python library used to interact with Git repositories.</p>
</li>
<li>
<p><a href="https://github.com/dgilland/shelmet">Shelmet</a> - A shell power-up for working with the file system and running subprocess commands.</p>
</li>
<li>
<p><a href="https://github.com/django-q2/django-q2">Django Q2</a> - A multiprocessing distributed task queue for Django based on Django-Q.</p>
</li>
</ul>
<p>Using Django might have been overkill, but having a Repository model to work with felt nice. Django Q2 was also overkill, but if I put this app into production, I’ll want a task queue, and Django Q2 has a manageable amount of overhead.</p>
<p>GitPython was a nice improvement over calling git commands directly because their API makes it easier to see which files were modified and to check against existing branch names. I was happy with the results after porting my subprocess commands to the GitPython API.</p>
<p>The final package I used is a new package called Shelmet, which was both a nice wrapper around subprocess plus they have a nice API for file system operations in the same vein as Python’s Pathlib module.</p>
<h2 id="future-goals">Future goals</h2>
<p>I was tempted to cobble together a GitHub bot, but I didn’t need one. I might dabble with the GitHub API more to fork a repo, but for now, this landed in a better place, so when I pick it back up again in a year, I’m starting in a good place.</p>
<p>If you want to write a GitHub bot, check out <a href="https://github.com/Mariatta">Mariatta</a>’s <a href="https://github.com/Mariatta/black_out">black_out</a> project.</p>
Automated Python and Django upgrades
https://micro.webology.dev/2024/03/22/automated-python-and-django-upgrades/
Fri, 22 Mar 2024 10:02:57 -0600http://webology.micro.blog/2024/03/22/automated-python-and-django-upgrades/<p>Recently, I have been maintaining forks for several projects that are no longer maintained. Usually, these are a pain to update, but I have found a workflow that takes the edge off by leveraging <a href="https://github.com/pre-commit/pre-commit">pre-commit</a>.</p>
<p>My process:</p>
<ul>
<li>Fork the project on GitHub to whichever organization I work with or my personal account.</li>
<li>Check out a local copy of my forked copy with git.</li>
<li>Install <a href="https://github.com/pre-commit/pre-commit">pre-commit</a></li>
<li>Create a <code>.pre-commit-config.yaml</code> with <em>ZERO</em> formatting or lint changes. This file will only include <a href="https://github.com/adamchainz/django-upgrade">django-upgrade</a> and <a href="https://github.com/asottile/pyupgrade">pyupgrade</a> hooks.</li>
</ul>
<p>We skip the formatters and linters to avoid unnecessary changes if we want to open a pull request in the upstream project. If the project isn’t abandoned, we will want to do that.</p>
<ul>
<li>For <a href="https://github.com/adamchainz/django-upgrade">django-upgrade</a>, change theโ-target-version option to target the latest version of Django I’m upgrading to, which is currently 5.0.</li>
<li>For <a href="https://github.com/asottile/pyupgrade">pyupgrade</a>, update the <code>python</code> settings under <code>default_language_version</code> to the latest version of Python that I’m targetting. Currently, that’s 3.12.</li>
</ul>
<p>The django-upgrade and pyupgrade projects attempt to run several code formatters and can handle most of the more tedious upgrade steps.</p>
<ul>
<li>Run <code>pre-commit autoupdate</code> to ensure we have the latest version of our hooks.</li>
<li>Run <code>pre-commit run --all-files</code> to run <code>pyupgrade</code> and <code>django-upgrade</code> on our project.</li>
<li>Run any tests contained in the project and review all changes.</li>
<li>Once I’m comfortable with the changes, I commit them all via git and push them upstream to my branch.</li>
</ul>
<h2 id="example-pre-commit-configyaml-config">Example <code>.pre-commit-config.yaml</code> config</h2>
<p>From my experience, less is more with this bane bones <code>.pre-commit-config.yaml</code> config file.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># .pre-commit-config.yaml</span>
<span style="color:#f92672">default_language_version</span>:
<span style="color:#f92672">python</span>: <span style="color:#ae81ff">python3.12</span>
<span style="color:#f92672">repos</span>:
- <span style="color:#f92672">repo</span>: <span style="color:#ae81ff">https://github.com/asottile/pyupgrade</span>
<span style="color:#f92672">rev</span>: <span style="color:#ae81ff">v3.15.1</span>
<span style="color:#f92672">hooks</span>:
- <span style="color:#f92672">id</span>: <span style="color:#ae81ff">pyupgrade</span>
- <span style="color:#f92672">repo</span>: <span style="color:#ae81ff">https://github.com/adamchainz/django-upgrade</span>
<span style="color:#f92672">rev</span>: <span style="color:#ae81ff">1.16.0</span>
<span style="color:#f92672">hooks</span>:
- <span style="color:#f92672">id</span>: <span style="color:#ae81ff">django-upgrade</span>
<span style="color:#f92672">args</span>: [--<span style="color:#ae81ff">target-version, "5.0"]</span>
</code></pre></div><p>If I’m comfortable that the project is abandoned, I’ll add <a href="https://github.com/astral-sh/ruff">ruff</a> support with a more opinionated config to ease my maintenance burden going forward.</p>
Justfile Alfred Plugin
https://micro.webology.dev/2024/03/19/justfile-alfred-plugin/
Tue, 19 Mar 2024 11:49:25 -0600http://webology.micro.blog/2024/03/19/justfile-alfred-plugin/<p>A few years back, I had a productivity conversation with <a href="https://kjaymiller.com">Jay Miller</a> about <a href="https://www.alfredapp.com">Alfred</a> plugins, which led to him sharing his <a href="https://github.com/kjaymiller/Bunch_Alfred">Bunch_Alfred</a> plugin. At the time, I played around with the <a href="https://bunchapp.co">Bunch.app</a>, a macOS automation tool, and Alfred’s support was interesting.</p>
<p>I created my Alfred plugin to run <a href="https://github.com/casey/just">Just</a> command runner commands through my Alfred setup. However, I never got around to packing or writing the plugin’s documentation.</p>
<p>My Alfred plugin runs <a href="https://www.alfredapp.com/help/workflows/inputs/script-filter/">Script Filter Input</a>, which reads from a centrally located <code>justfile</code> and generates JSON output of all of the possible options. This will be displayed, and Alfred will run that command, whichever option you select.</p>
<img src="https://cdn.uploads.micro.blog/137490/2024/screenshot-2024-03-19-at-12.37.56-pm.png" width="600" height="403" alt="Alfred plugin showing a Just command with a list of recipe options to pick from.">
<p>I was always unhappy with how the JSON document was generated from my commands, so I dusted off the project over lunch and re-engineered it by adding <a href="https://github.com/pydantic/pydantic">Pydantic</a> support.</p>
<p>Alfred just announced support for a new User Interface called <a href="https://www.alfredapp.com/help/workflows/user-interface/text/">Text View</a>, which could make text and markdown output from Python an exciting way to handle snippets and other productive use cases. I couldn’t quite figure it out over lunch, but now I know it’s possible, and I might figure out how to convert my Justfile Alfred plugin to generate better output.</p>
<script src="https://gist.github.com/jefftriplett/e7d4eade12e30001065eed2636010772.js"></script>
Weeknotes for Week 11: March 11 to March 17
https://micro.webology.dev/2024/03/18/weeknotes-for-week-march-to/
Mon, 18 Mar 2024 18:33:40 -0600http://webology.micro.blog/2024/03/18/weeknotes-for-week-march-to/<h2 id="family">Family</h2>
<p>This week was Spring Break for both of my kids. My son had all week off, while my daughter had Thursday and Friday off. My son was also sick and hanging out with me the first two days of the week instead of getting to go to day camp.</p>
<p>I worked on several house projects, including cleaning our smoker and outdoor griddle. I didn’t winterize our smoker nearly as well as I thought I had, so it took me much longer to clean than it should have.</p>
<p>Sunday was cold and windy, so I couldn’t do as much outside as I planned.</p>
<h2 id="work">Work</h2>
<p>I spent most of my week working on the green field project again.</p>
<p>I also hosted a rather minute(ish)ย <a href="https://mastodon.social/@webology/112096980187024682">Office Hours</a> session again.</p>
<h2 id="community">Community</h2>
<p>I had two DjangoCon US meetings this week. One is to discuss our online experience, and the other is to kick off our new website and eventual new website framework.</p>
<h2 id="side-projects">Side projects</h2>
<p>I spent most of my side project time on <a href="https://django-news.com">Django News</a>, <a href="https://jobs.django-news.com">Django News Jobs</a>, and <a href="https://djangopackages.org">Django Packages</a>. I also upgraded several project configs to use some new <a href="https://github.com/astral-sh/uv">uv</a> options.</p>
<p><strong>Django Packages:</strong> We updated our profiles to drop support for Google Code and add Bitbucket and GitLab profiles.</p>
<p><strong>Trailhawks:</strong> My local running club’s website has been running on Django since 2009. I upgraded the project from Django 3.2 to 5.0 and to Python 3.1x, including upgrading two outdated third-party packages to Python 3.1x and Django 5. I also changed one of our race websites to Tailwind CSS, which has been in flight for a while.</p>
<p><strong>Django Packages:</strong> We released an update to our profiles to drop support for Google Code and to add Bitbucket and GitLab profiles. I also broke the website with a wsgi change, which I quickly found a fixed.</p>
<p>**django-startproject: ** I updatedย <a href="https://github.com/jefftriplett/django-startproject"><code>django-startproject</code></a>ย project to have better support for <code>uv</code>.</p>
<p><strong>Django News Newsletter:</strong>ย We shipped issueย <a href="https://django-news.com/issues/223#start">#223</a>.</p>
<h2 id="writing">Writing</h2>
<p>I wrote and published every day.</p>
<p><a href="https://micro.webology.dev/2024/03/17/on-tortilla-pizza.html">2024-03-17</a>:ย <strong>๐ On tortilla pizza</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/16/on-gardening.html">2024-03-16</a>:ย <strong>๐ก On Gardening</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/15/five-for-friyay.html">2024-03-15</a>:ย <strong>Five for Friyay</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/14/pythons-uv-tool.html">2024-03-14</a>:ย <strong>Python’s UV tool is even better</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/13/on-environment-variables.html">2024-03-13</a>:ย <strong>On environment variables and dotenv files</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/12/completed-finished-or.html">2024-03-12</a>:ย <strong>Completed, finished, or done</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/11/my-python-roots.html">2024-03-11</a>:ย <strong>My Python Roots</strong>ย </p>
<h2 id="me-time">“Me” time</h2>
<p>๐ I watched Kansas get swept in the first round of the Big 12 tournament because our best two players were out with injuries. I set aside time on Wednesday and Thursday afternoons to watch some conference tournaments, but they were all blowouts.</p>
<h2 id="next-week">Next week</h2>
<p>๐ธ My kids are back in school, and our schedules return to regular routines.</p>
<p>๐ช I have more rot repair, and I now have a compound miter saw to cut wood for framing and fixing things. I had to order a portable workbench to attach the miter saw in mid-week.</p>
<p>๐ Next week also starts the NCAA tournament, a sign that the third basketball season has begun. I am taking two days off to enjoy the madness, and I suspect this will be most of my weekend, too.</p>
<p>๐ I should have to get the other half of the Trailhawks website out the door.</p>
Five for Friyay
https://micro.webology.dev/2024/03/15/five-for-friyay/
Fri, 15 Mar 2024 19:05:35 -0600http://webology.micro.blog/2024/03/15/five-for-friyay/<p>It’s been too long since I posted my five favorite links of the week. Continuing that theme for today, here are five links from my Friyay (Friday).</p>
<h2 id="1-a-new-code-editor">1. A new code editor</h2>
<p>Tonight, I installed the <a href="https://zed.dev">Zed</a> code editor on a whim, and to my surprise, my LSP was automatically picked up and just worked. Zed was a surprisingly productive experience. I even spotted a bug in one of my projects that I needed help to figure out earlier today.</p>
<h2 id="2-my-favorite-theme">2. My favorite theme</h2>
<p>I wasn’t impressed with Zed’s default theme, so I searched the <a href="https://draculatheme.com">Dracula</a> website and found a <a href="https://draculatheme.com/zed">Dracula Theme for Zed</a>. Dracula is one of my favorite themes, and they have custom-built themes for over 370 different apps.</p>
<h2 id="3-a-new-to-me-python-library">3. A new to me Python library</h2>
<p><a href="https://github.com/dgilland/shelmet">shelmet</a> is a nice wrapper around Python’s subprocess. The API is clean, and it has a nice wrapper around using temporary environment variables, which has been helpful for me.</p>
<h2 id="4-the-best-presentation-tool">4. The best presentation tool</h2>
<p><a href="https://www.deckset.com">Deckset</a> is the best tool for quickly writing nice-looking presentations using markdown. After years of being in private beta, they released their iOS/iPad version publicly.</p>
<h2 id="5-a-long-read-for-the-weekend">5. A long read for the weekend</h2>
<p>This exhaustive review of the <a href="https://hugo.blog/2024/03/11/vision-pro/">Vision Pro</a> is everything I have been looking for and more to get a feel for what the Vision Pro can do.</p>
<h2 id="have-thoughts">Have thoughts?</h2>
<iframe src="https://mastodon.social/@webology/112102679318546214/embed" class="mastodon-embed" style="max-width: 100%; border: 0" width="400" allowfullscreen="allowfullscreen"></iframe><script src="https://mastodon.social/embed.js" async="async"></script>
Python's UV tool is even better
https://micro.webology.dev/2024/03/14/pythons-uv-tool-is-even/
Thu, 14 Mar 2024 10:51:21 -0600http://webology.micro.blog/2024/03/14/pythons-uv-tool-is-even/<p>Last month, I wrote <a href="https://micro.webology.dev/2024/02/29/pythons-uv-tool.html">Python’s UV tool is actually pretty good</a> about Astral’s new Python package installer and resolver <a href="https://github.com/astral-sh/uv"><code>uv</code></a>, and this is a follow-up post.</p>
<p>Since last month, I have added <code>uv</code> to over a dozen projects, and I recently learned that you could skip the <code>venv</code> step for projects that use containers or CI where the environment is already isolated.</p>
<p>I mistakenly thought <code>uv</code> required a virtual environment (aka venv), but <a href="https://joshthomas.dev">Josh Thomas</a> recently pointed out that it’s unnecessary.</p>
<p>The trick is to pass the <code>--system</code> option, and <code>uv</code> will perform a system-wide install. Here’s an example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">uv pip install --system --requirement requirement.txt
</code></pre></div><p>Now that I have seen this, I wish <code>pip</code> also used this approach to avoid developers accidentally installing third-party packages globally.</p>
<h2 id="local-development">local development</h2>
<p>Nothing has changed with my <code>justfile</code> example from last month.</p>
<p>When I’m working with containers, I create a virtual environment (venv) because I will need most of my project requirements installed <em>outside</em> of the container so that my text editor and LSP can resolve dependencies. <code>uv</code>’s default behavior of respecting a <code>venv</code> is all we need here.</p>
<p>Every one of my projects has a <code>justfile</code> (it’s like Make but works the same everywhere) with “bootstrap” and “lock” recipes. My “bootstrap” recipe installs everything I need to work with the project locally. I use my “lock” recipe to lock my <code>requirements.txt</code> file to use the exact requirements locally and in production.</p>
<h3 id="justfile-before"><code>justfile</code> before</h3>
<p>My <code>justfile</code> might look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">@<span style="color:#ae81ff">bootstrap</span>
<span style="color:#ae81ff">python -m pip install --upgrade pip</span>
<span style="color:#ae81ff">python -m pip install --upgrade --requirement requirements.in</span>
<span style="color:#f92672">@lock *ARGS</span>:
<span style="color:#ae81ff">python -m piptools compile {{ ARGS }} ./requirements.in \</span>
--<span style="color:#ae81ff">resolver=backtracking \</span>
--<span style="color:#ae81ff">output-file requirements.txt</span>
</code></pre></div><h3 id="justfile-after"><code>justfile</code> after</h3>
<p>For the most part, <code>uv</code> shares most of the same syntax as <code>pip</code> so you can start by changing your <code>pip</code> references to <code>uv pip</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">@<span style="color:#ae81ff">bootstrap</span>
<span style="color:#ae81ff">python -m pip install --upgrade pip uv</span>
<span style="color:#ae81ff">python -m uv pip install --upgrade --requirement requirements.in</span>
<span style="color:#f92672">@lock *ARGS</span>:
<span style="color:#ae81ff">python -m uv pip compile {{ ARGS }} requirements.in \</span>
--<span style="color:#ae81ff">resolver=backtracking \</span>
--<span style="color:#ae81ff">output-file requirements.txt</span>
</code></pre></div><h2 id="dockerfiles">Dockerfiles</h2>
<p>Everyone’s container setup is going to be different, but I use <a href="https://www.docker.com">Docker</a> and <a href="https://orbstack.dev">Orbstack</a>, which use a <code>Dockerfile</code>.</p>
<h2 id="dockerfile-before"><code>Dockerfile</code> before</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-Dockerfile" data-lang="Dockerfile"><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> python:3.12-slim-bookworm</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PIP_DISABLE_PIP_VERSION_CHECK <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PYTHONDONTWRITEBYTECODE <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PYTHONPATH /srv<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PYTHONUNBUFFERED <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> apt-get update<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> pip install --upgrade pip<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> requirements.txt /src/requirements.txt<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> pip install --requirement /src/requirements.txt<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">WORKDIR</span><span style="color:#e6db74"> /src/</span><span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><h2 id="dockerfile-after"><code>Dockerfile</code> after</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-Dockerfile" data-lang="Dockerfile"><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> python:3.12-slim-bookworm</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PIP_DISABLE_PIP_VERSION_CHECK <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PYTHONDONTWRITEBYTECODE <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PYTHONPATH /srv<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PYTHONUNBUFFERED <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> apt-get update<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> pip install --upgrade pip uv <span style="color:#75715e"># this is updated</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> requirements.txt /src/requirements.txt<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> uv pip install --system --requirement /src/requirements.txt <span style="color:#75715e"># this is updated</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">WORKDIR</span><span style="color:#e6db74"> /src/</span><span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><h2 id="github-actions">GitHub Actions</h2>
<p>GitHub Actions are a little more complicated to explain, but my workflows started similar to this before I made the switch to <code>uv</code>:</p>
<h3 id="mainyml-before"><code>main.yml</code> before</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Set up Python 3.12</span>
<span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-python@v5</span>
<span style="color:#f92672">with</span>:
<span style="color:#f92672">python-version</span>: <span style="color:#e6db74">'3.12'</span>
- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Install dependencies</span>
<span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span><span style="color:#e6db74"> </span> <span style="color:#ae81ff">python -m pip install --requirement requirements.in</span>
</code></pre></div><h3 id="mainyml-after"><code>main.yml</code> after</h3>
<p>The most significant pain point I ran into was related to GitHub Issue <a href="https://github.com/astral-sh/uv/issues/1386">#1386</a>, which has a useable workaround.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"> - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Set up Python 3.12</span>
<span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-python@v5</span>
<span style="color:#f92672">with</span>:
<span style="color:#f92672">python-version</span>: <span style="color:#e6db74">'3.12'</span>
- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Install dependencies</span>
<span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span><span style="color:#e6db74"> python -m pip install --upgrade uv # this is new
</span><span style="color:#e6db74"> python -m uv pip install --system --requirement requirements.in # this is updated</span>
</code></pre></div><h2 id="gotchas">Gotchas</h2>
<p>The only gotchas I have encountered with <code>uv</code> is when I’m trying to install a Python package from a remote zip file.</p>
<p>Previously, I could copy and paste the GitHub repo URL, but <code>uv</code> required we use the format <code>package-name @ url-to-zip-file</code></p>
<h3 id="requirementsin-before"><code>requirements.in</code> before</h3>
<pre tabindex="0"><code># requirements.in
https://github.com/jefftriplett/django-feedreader/archive/main.zip
</code></pre><h3 id="requirementsin-after"><code>requirements.in</code> after</h3>
<pre tabindex="0"><code># requirements.in
django-feedreader @ https://github.com/jefftriplett/django-feedreader/archive/main.zip
</code></pre><h2 id="conclusion">Conclusion</h2>
<p>This update helps remove a few steps from updating your projects, and it should shave a few minutes off of updating projects to use it.</p>
<p>I hope this was helpful to anyone who is considering making the switch to <code>uv</code>. I love to hear about how much time it saves you.</p>
On environment variables and dotenv files
https://micro.webology.dev/2024/03/13/on-environment-variables-and-dotenv/
Wed, 13 Mar 2024 20:23:26 -0600http://webology.micro.blog/2024/03/13/on-environment-variables-and-dotenv/<p>Brett Cannon recently vented some frustrations about <code>.env</code> files.</p>
<blockquote>
<p>I still hate .env files and their lack of a standard</p>
<p><a href="https://mastodon.social/@[email protected]/112056455108582204">https://mastodon.social/@[email protected]/112056455108582204</a></p>
</blockquote>
<p>Brett’s thread and our conversation reminded me that my rule for working with dotenv files is to have my environment load them instead of my Python app trying to read from the <code>.env</code> file directly.</p>
<h2 id="what-is-a-env-dotenv-file">What is a <code>.env</code> (dotenv) file?</h2>
<p>A <code>.env</code> (aka dotenv) is a file that contains a list of key-value pairs in the format of <code>{key}=value</code>.</p>
<p>At a basic level, this is what a bare minimum <code>.env</code> file might look for in a Django project.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#75715e"># .env</span>
DEBUG<span style="color:#f92672">=</span>true
SECRET_KEY<span style="color:#f92672">=</span>you need to change this
</code></pre></div><p>My go-to library for reading ENV variables is <a href="https://github.com/sloria/environs"><code>environs</code></a>. While the <code>environs</code> library can read directly from a dotenv file, don’t do that. I never want my program to read from a file in production because I don’t want a physical file with all of my API keys and secrets.</p>
<p>Most hosting providers, like <a href="https://fly.io">Fly.io</a>, have a command line interface for setting these key-value pairs in production to avoid needing a physical dotenv file.</p>
<p>Instead, we should default to assuming that the ENV variables will bet in our environment, and we should fall back to either a reasonable default value or fail loudly.</p>
<p>Using the <code>environs</code> library, my Django <code>settings.py</code> file tends to look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># settings.py</span>
<span style="color:#f92672">import</span> environs
env <span style="color:#f92672">=</span> environs<span style="color:#f92672">.</span>Env()
<span style="color:#75715e"># this will default to False if not set.</span>
DEBUG <span style="color:#f92672">=</span> env<span style="color:#f92672">.</span>bool(<span style="color:#e6db74">"DJANGO_DEBUG"</span>, default<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
<span style="color:#75715e"># this will error loudly if not set</span>
SECRET_KEY <span style="color:#f92672">=</span> env<span style="color:#f92672">.</span>str(<span style="color:#e6db74">"SECRET_KEY"</span>)
<span style="color:#75715e"># everything else... </span>
</code></pre></div><p>I lean on Docker Compose for local development when I’m building web apps because I might have three to five services running. Compose can read a dotenv file and register them into environment variables.</p>
<h2 id="envrc-files-arent-env-files"><code>.envrc</code> files aren’t <code>.env</code> files</h2>
<p>On my macOS, when I’m not developing in a container, I use the <a href="https://direnv.net"><code>direnv</code></a> application to read an <code>.envrc</code> file which is very similar to a dotenv file.</p>
<p>A <code>.envrc</code> is very similar to a <code>.env</code> file, but to register the values into memory, you have to use Bash’s <code>export</code> convention. If you don’t specify <code>export</code>, the environment variables won’t be available in your existing Bash environment.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#75715e"># .envrc</span>
export DEBUG<span style="color:#f92672">=</span>true
export SECRET_KEY<span style="color:#f92672">=</span>you need to change this
</code></pre></div><p>I’m a fan of <code>direnv</code> because the utility ensures that my environment variables are only set while I am in the same folder or sub-folders that contain the <code>.envrc</code> file. If I move to a different folder location or project, <code>direnv</code> will automatically unload every environment variable that was previously set.</p>
<p>This has saved me numerous times over the years when I have run a command that might upload a file to s3 and ensure that I’m not uploading to the wrong account because an environment variable is still set from another project.</p>
<p>Clients are generally understanding, but overriding static media for one client with another client’s files is not a conversation I want to have with any client.</p>
<p><code>direnv</code> is excellent insurance against forgetting to unset an environment variable.</p>
<h2 id="seeding-a-env-file">Seeding a <code>.env</code> file</h2>
<p>I prefer to ship an example <code>.env.example</code> file in my projects with reasonable defaults and instructions for copying them over.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#75715e"># .env.example</span>
DEBUG<span style="color:#f92672">=</span>true
SECRET_KEY<span style="color:#f92672">=</span>you need to change this
</code></pre></div><p>If you are a <a href="https://github.com/casey/just"><code>casey/just</code></a> <code>justfile</code> user, I like to ship a <code>just bootstrap</code> recipe that checks if a <code>.env</code> file already exists. If the <code>.env</code> file does not exist, it will copy the example in place.</p>
<p>My <code>bootstrap</code> recipe typically looks like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml"><span style="color:#75715e"># justfile</span>
<span style="color:#f92672">bootstrap *ARGS</span>:
<span style="color:#75715e">#!/usr/bin/env bash</span>
<span style="color:#ae81ff">set -euo pipefail</span>
<span style="color:#ae81ff">if [ ! -f ".env" ]; then</span>
<span style="color:#ae81ff">echo ".env created"</span>
<span style="color:#ae81ff">cp .env.example .env</span>
<span style="color:#ae81ff">fi</span>
</code></pre></div><h2 id="how-do-we-keep-dotenv-files-in-sync">How do we keep dotenv files in sync?</h2>
<p>One pain point when working with dotenv files is keeping new environment variables updated when a new variable has been added.</p>
<p>Thankfully, <a href="https://github.com/kurtbuilds/modenv">modenv</a> is an excellent utility that can do precisely this. I run <code>modenv check</code> and will compare the <code>.env*</code> files in the existing folder. It will tell us which files are missing an environment variable when it exists in one but not one of the other files.</p>
<p>I use <code>modenv check -f</code> to sync up any missing keys with a blank value. This works well to sync up any new environment variables added to our <code>.env.example</code> file with our local <code>.env</code> file.</p>
<h2 id="alternatives">Alternatives</h2>
<p>I recently wrote about <a href="https://micro.webology.dev/2024/02/21/using-chamber-with.html">Using Chamber with Django and managing environment variables</a>, which dives into using Chamber, another tool for managing environment variables.</p>
<p>If you are working with a team, the <a href="https://developer.1password.com/docs/cli/secrets-environment-variables/">1Password CLI</a>’s <code>op run</code> command is an excellent way to share environment variables securely. The tool is straightforward and can be integrated securely with local workflows and CI with just a few steps.</p>
My Python Roots
https://micro.webology.dev/2024/03/11/my-python-roots/
Mon, 11 Mar 2024 18:49:57 -0600http://webology.micro.blog/2024/03/11/my-python-roots/<p>Last week, during office hours, I shared the two libraries that were my gateways to learning Python.</p>
<h2 id="cog">Cog</h2>
<p>I stumbled on Ned Batchelder’s <a href="https://nedbatchelder.com/code/cog/">Cog</a> while running an ISP in SWMO in the mid-00s. At the time, I was writing lots of PHP code and had a few layers of ORM code that I could generate with Cog’s help. This code was mainly boilerplate, and Cog was great at templating code. Thankfully, I didn’t need to know Python with Cog to make it work.</p>
<p>In recent years, I have still used Cog to update docs and to document Justfiles, Click, Typer, and console apps by grabbing the output and embedding it into docs.</p>
<h2 id="beautiful-soup">Beautiful Soup</h2>
<p><a href="https://www.crummy.com/software/BeautifulSoup/">Beautiful Soup</a> is the library that pushed me to learn Python.
Beautiful Soup motivated me to learn Python and even more advanced feats like installing LXML and processing unparseable HTML or XML.
I have always liked writing web scrapers and processing HTML documents, which is a weird hobby of mine.</p>
<h2 id="my-first-python-app">My first Python app</h2>
<p>My friends and I worked in our first post-college dot com job, and Dell was running an incredible deal on their 20" widescreen monitors over the Christmas holiday.</p>
<p>Dell ran a daily Dell Elf (Delf) contest where you gave them your email address, and they would give you a discount code for their various products.</p>
<p>The best code was 50% off of their 20" widescreen displays, which was an incredible deal then. The display retailed for $499, so getting one for $249.50 was great. These codes were random, and the odds were 1 in 25 to get one.</p>
<p>Using Python and having an email catchall, I wrote my first script to submit a series of email addresses until we found the daily 50% off code. At least four or five of my friends and I stocked up on these monitors that fall, and I have been a fan of Dell displays ever since.</p>
<h2 id="today">Today</h2>
<p>I still use Cog and Beautiful Soup 4 in several projects, including a few daily drivers. Last year, during their end-of-year sale, I picked three Dell 27-inch displays, and I still have fond memories of Dell’s displays.</p>
Weeknotes 2024 W10
https://micro.webology.dev/2024/03/10/weeknotes-w/
Sun, 10 Mar 2024 19:06:03 -0600http://webology.micro.blog/2024/03/10/weeknotes-w/<p>We were reminded that Winter is still here this week, starting with warmer weather and ending with colder.</p>
<h2 id="family">Family</h2>
<p>My oldest was out two days this week at parent-teacher conferences, and next week, he has off for Spring Break. He missed his last basketball practice and game because he’s had a bug for a few days with a runny nose and a low fever that keeps coming back.</p>
<p>My youngest child is firmly in the four to six-word sentence phase. We are up to our eyeballs in potty training, and my daughter’s new favorite phrase is, “No, not yet,” when you suggest we try again.</p>
<p>The kid’s grandfather drove down from Chicago to spend the weekend with us.</p>
<p>I picked up Oliver’s bug on Saturday and spent it more down than up, so my new compound miter saw sat in the box all weekend.</p>
<h2 id="work">Work</h2>
<p>We are a few weeks into a new client project, and that’s led to several refactors and working with newer tools, including Pydantic and Django Ninja for Model Schemas. I also expect to spend much time in Tailwind CSS land soon.</p>
<h2 id="community">Community</h2>
<p>We had our monthly DEFNA meeting, elected officers, and discussed upcoming announcements.</p>
<p>I had a PSF Code of Conduct WG meeting that conflicted with morning kid drop-offs, but I could join for the last 15 to 20 minutes to help them make a quorum so we could vote to pass a few things.</p>
<p>I only had time to join the first half of oneย <a href="https://wiggle.work/">Wiggle Work</a>ย event this week over lunch.</p>
<p>This week, I hostedย <a href="https://mastodon.social/@webology/112049364089333842">Office Hours</a>.</p>
<h2 id="side-projects">Side projects</h2>
<p>I spent most of my side project time on <a href="https://django-news.com">Django News</a>, <a href="https://jobs.django-news.com">Django News Jobs</a>, and <a href="https://djangopackages.org">Django Packages</a> and upgraded several projects to the latest Django 5.0.x security release and Python 3.12 releases. I also moved several projects to use <a href="https://github.com/astral-sh/uv">uv</a> Python package manager, including <a href="https://djangopackages.org">Django Packages</a> and <a href="https://jobs.django-news.com">Django News Jobs</a>, which shaves minutes off of build times.</p>
<p><strong>Django News Newsletter:</strong>ย We shipped issueย <a href="https://django-news.com/issues/222#start">#222</a>.</p>
<p>I started on a Bootstrap to Tailwind CSS conversion tool after noticing that several popular tools last updated 3 to 6 years ago. I can brute force about 90% of it, but I have also meant to play around with <a href="https://pypi.org/project/cssutils/"><code>cssutils</code></a> for a while. With <code>cssutils</code>, I think it might be possible to read from the various Bootstrap CSS versions and match up to some Tailwind CSS classes by comparing them via their CSS output.</p>
<h2 id="writing">Writing</h2>
<p>I wrote and published every day.</p>
<p><a href="https://micro.webology.dev/2024/03/09/bootstrap-to-tailwind.html">2024-03-09</a>:ย <strong>Bootstrap to Tailwind CSS</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/08/dave-the-diver.html">2024-03-08</a>:ย <strong>๐ฎ Dave the Diver</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/07/how-to-test.html">2024-03-07</a>:ย <strong>How to test with Django, parametrize, and lazy fixtures</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/06/importing-data-with.html">2024-03-06</a>:ย <strong>Importing data with Django Ninja’s ModelSchema</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/04/updating-python-from.html">2024-03-05</a>:ย <strong>Upgrading Python from 3.11 to 3.12 notes</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/04/pip-isolation.html">2024-03-04</a>:ย <strong>On pip isolation</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/03/03/the-weekend-woodworker.html">2024-03-03</a>:ย <strong>The Weekend Woodworker</strong>ย ๐ช</p>
<h2 id="me-time">“Me” time</h2>
<p>๐ I watched two Kansas Basketball games, a bunch of random men’s and primarily women’s games.</p>
<h2 id="next-week">Next week</h2>
<p>I have more rot repair, and I now have a compound miter saw to make cutting wood for framing much more manageable.</p>
<p>My oldest is off with spring break, so our morning routines start 10 minutes later and last about 40 minutes longer.</p>
<p>Next week also starts the Big 12 Men’s tournament, a sign that the second and third basketball seasons are beginning. I usually take a day off for the start of the Big 12 tournament, and I take a few days off for March Madness.</p>
Bootstrap to Tailwind CSS
https://micro.webology.dev/2024/03/09/bootstrap-to-tailwind-css/
Sat, 09 Mar 2024 22:14:27 -0600http://webology.micro.blog/2024/03/09/bootstrap-to-tailwind-css/<p>I spent a few hours tonight weighing my options to port a few websites from Bootstrap to Tailwind CSS.</p>
<p>I started with what seems to be the original <a href="https://github.com/awssat/tailwindo"><code>awssat/tailwindo</code></a> project is a PHP console app whose goal was to convert any Bootstrap to Tailwind CSS and was last updated three years ago. I couldn’t get it to work from the console or via Docker, so I punted and looked at other options.</p>
<p>This led me to the <a href="https://github.com/riazXrazor/node-tailwindo"><code>node-tailwindo</code></a> project, which did install successfully for me. <code>node-tailwindo</code> project hadn’t been updated in six years, so much has changed in both projects.</p>
<p>Since <code>node-tailwindo</code> was installed successfully and seemed to run OK, I ran it on a few projects, including <a href="https://djangopackages.org">Django Packages</a>, and the results were not terrible. They were not amazing, but things worked.</p>
<p>I looked at commercial options, and they fall into either Browser Extensions that let you view an existing website with a copy/convert to Tailwind CSS option or tools that rewrite your existing CSS. Neither felt like a good option to me.</p>
<p>I finally did what any Python developer would and installed <a href="https://beautiful-soup-4.readthedocs.io/en/latest/">BeautifulSoup4</a>. Next, I wrote a script to read all the files in a template folder, and it extracted all the class attributes from the existing HTML. One hundred seventy-six unique classes later, I had my answer.</p>
<p>Writing my upgrade tool felt like a bigger project that I wanted to take on, but it helped me spot a few issues that <code>node-tailwindo</code> would struggle with.</p>
<p>This is where BeautifulSoup4 shines, and I could quickly swap out a few classes before I fed them into <code>node-tailwindo</code>, and it fixes several bugs where the project was confused by <code>{% block %}</code> and <code>{{ variable }}</code> tags/blocks.</p>
<p>This might be a project; I slowly update as I get bored since I can probably add and test 10 to 20 tests over lunch. For a brief minute, I debated if this would be my first Rust app. Spoiler: It is not.</p>
How to test with Django, parametrize, and lazy fixtures
https://micro.webology.dev/2024/03/07/how-to-test-with-django/
Thu, 07 Mar 2024 19:53:36 -0600http://webology.micro.blog/2024/03/07/how-to-test-with-django/<p>This article is a follow-up to my post on <a href="https://micro.webology.dev/2024/02/05/how-to-test.html">How to test with Django and pytest fixtures</a>.</p>
<p>Here are some notes on how I prefer to test views for a Django application with authentication using <a href="https://github.com/TvoroG/pytest-lazy-fixture">pytest-lazy-fixture</a>.</p>
<h2 id="fixtures">Fixtures</h2>
<p><code>pytest-django</code> has a <a href="https://pytest-django.readthedocs.io/en/latest/helpers.html#django-user-model"><code>django_user_model</code></a> fixture/shortcut, which I recommend using to create valid Django user accounts for your project.</p>
<p>This example assumes that there are four levels of users. We have anonymous (not authenticated), “user,” staff, and superuser levels of permission to work with. Both staff and superusers follow the Django default pattern and have the <code>is_staff</code> and <code>is_superuser</code> boolean fields set appropriately.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># users/tests/fixtures.py</span>
<span style="color:#f92672">import</span> pytest
<span style="color:#a6e22e">@pytest</span><span style="color:#f92672">.</span>fixture
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">password</span>(db) <span style="color:#f92672">-></span> str:
<span style="color:#66d9ef">return</span> <span style="color:#e6db74">"password"</span>
<span style="color:#a6e22e">@pytest</span><span style="color:#f92672">.</span>fixture
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">staff</span>(db, django_user_model, faker, password):
<span style="color:#66d9ef">return</span> django_user_model<span style="color:#f92672">.</span>objects<span style="color:#f92672">.</span>create_user(
email<span style="color:#f92672">=</span><span style="color:#e6db74">"[email protected]"</span>,
first_name<span style="color:#f92672">=</span>faker<span style="color:#f92672">.</span>first_name(),
is_staff<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
is_superuser<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
last_name<span style="color:#f92672">=</span>faker<span style="color:#f92672">.</span>last_name(),
password<span style="color:#f92672">=</span>password,
)
<span style="color:#a6e22e">@pytest</span><span style="color:#f92672">.</span>fixture()
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">superuser</span>(db, django_user_model, faker, password):
<span style="color:#66d9ef">return</span> django_user_model<span style="color:#f92672">.</span>objects<span style="color:#f92672">.</span>create_user(
email<span style="color:#f92672">=</span><span style="color:#e6db74">"[email protected]"</span>,
first_name<span style="color:#f92672">=</span>faker<span style="color:#f92672">.</span>first_name(),
is_staff<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
is_superuser<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
last_name<span style="color:#f92672">=</span>faker<span style="color:#f92672">.</span>last_name(),
password<span style="color:#f92672">=</span>password,
)
<span style="color:#a6e22e">@pytest</span><span style="color:#f92672">.</span>fixture()
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">user</span>(db, django_user_model, faker, password):
<span style="color:#66d9ef">return</span> django_user_model<span style="color:#f92672">.</span>objects<span style="color:#f92672">.</span>create_user(
email<span style="color:#f92672">=</span><span style="color:#e6db74">"[email protected]"</span>,
first_name<span style="color:#f92672">=</span>faker<span style="color:#f92672">.</span>first_name(),
last_name<span style="color:#f92672">=</span>faker<span style="color:#f92672">.</span>last_name(),
password<span style="color:#f92672">=</span>password,
)
</code></pre></div><h2 id="testing-our-views-with-different-user-roles">Testing our views with different User roles</h2>
<p>We will assume that our website has some working Category pages that can only viewed by staff or superusers. The <code>lazy_fixture</code> library allows us to pass the name of a fixture using parametrize along with the expected status_code that our view should return.</p>
<p>If you have never seen <code>parametrize</code>, it is a nice pytest convention that will re-run the same test multiple times while passing a list of parameters into the test to be evaluated.</p>
<p>The <code>tp</code> function variable is a <a href="https://github.com/revsys/django-test-plus">django-test-plus</a> fixture.</p>
<p><code>user</code>, <code>staff</code>, and <code>superuser</code> are fixtures we created above.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># categories/tests/test_views.py</span>
<span style="color:#f92672">import</span> pytest
<span style="color:#f92672">from</span> pytest <span style="color:#f92672">import</span> param
<span style="color:#f92672">from</span> pytest_lazyfixture <span style="color:#f92672">import</span> lazy_fixture
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">test_category_noauth</span>(db, tp):
<span style="color:#e6db74">"""
</span><span style="color:#e6db74"> GET 'admin/categories/'
</span><span style="color:#e6db74"> """</span>
url <span style="color:#f92672">=</span> tp<span style="color:#f92672">.</span>reverse(<span style="color:#e6db74">"admin:category-list"</span>)
<span style="color:#75715e"># Does this view work with auth?</span>
response <span style="color:#f92672">=</span> tp<span style="color:#f92672">.</span>get(url)
tp<span style="color:#f92672">.</span>response_401(response)
<span style="color:#a6e22e">@pytest</span><span style="color:#f92672">.</span>mark<span style="color:#f92672">.</span>parametrize(
<span style="color:#e6db74">"testing_user,status_code"</span>,
[
param(lazy_fixture(<span style="color:#e6db74">"user"</span>), <span style="color:#ae81ff">403</span>),
param(lazy_fixture(<span style="color:#e6db74">"staff"</span>), <span style="color:#ae81ff">200</span>),
param(lazy_fixture(<span style="color:#e6db74">"superuser"</span>), <span style="color:#ae81ff">200</span>),
],
)
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">test_category_with_auth</span>(db, tp, testing_user, password, status_code):
<span style="color:#e6db74">"""
</span><span style="color:#e6db74"> GET 'admin/categories/'
</span><span style="color:#e6db74"> """</span>
url <span style="color:#f92672">=</span> tp<span style="color:#f92672">.</span>reverse(<span style="color:#e6db74">"admin:category-list"</span>)
<span style="color:#75715e"># Does this view work with auth?</span>
tp<span style="color:#f92672">.</span>client<span style="color:#f92672">.</span>login(username<span style="color:#f92672">=</span>testing_user<span style="color:#f92672">.</span>email, password<span style="color:#f92672">=</span>password)
response <span style="color:#f92672">=</span> tp<span style="color:#f92672">.</span>get(url)
<span style="color:#66d9ef">assert</span> response<span style="color:#f92672">.</span>status_code <span style="color:#f92672">==</span> status_code
</code></pre></div><h2 id="notes">Notes</h2>
<p><strong>Please note:</strong> These status codes are more typical for a REST API. So I would adjust any 40x status codes accordingly.</p>
<p>My goal in sharing these examples is to show that you can get some helpful testing in with a little bit of code, even if the goal isn’t to dive deep and cover everything.</p>
<h2 id="updates">Updates</h2>
<p>To make my example more consistent, I updated <code>@pytest.mark.django_db()</code> to use a <code>db</code> fixture. Thank you, <a href="https://mastodon.social/@[email protected]/112060829098386312">Ben Lopatin</a>, for the feedback.</p>
Importing data with Django Ninja's ModelSchema
https://micro.webology.dev/2024/03/06/importing-data-with-django-ninjas/
Wed, 06 Mar 2024 21:54:11 -0600http://webology.micro.blog/2024/03/06/importing-data-with-django-ninjas/<p>I have recently been playing with <a href="https://github.com/vitalik/django-ninja">Django Ninja</a> for small APIs and for leveraging Schema. Specifically, <a href="https://django-ninja.dev/guides/response/django-pydantic/">ModelSchema</a> is worth checking out because it’s a hidden gem for working with Django models, even if you aren’t interested in building a Rest API.</p>
<blockquote>
<p>Schemas are very useful to define your validation rules and responses, but sometimes you need to reflect your database models into schemas and keep changes in sync.
<a href="https://django-ninja.dev/guides/response/django-pydantic/">https://django-ninja.dev/guides/response/django-pydantic/</a></p>
</blockquote>
<p>One challenge we face is importing data from one legacy database into a new database with a different structure. While we can map old fields to new fields using a Python dictionary, we also need more control over what the data looks like coming back out.</p>
<p>Thankfully, ModelSchema is built on top of <a href="%5BPydantic%5D(https://docs.pydantic.dev/latest/)">Pydantic</a>’s <a href="https://docs.pydantic.dev/2.6/api/base_model/">BaseModel</a> and supports Pydantic’s <a href="https://docs.pydantic.dev/latest/api/fields/#pydantic.fields.Field">Field alias</a> feature.</p>
<p>This allows us to create a ModelSchema based on a LegacyCategory model, and we can build out <code>Field(alias="...")</code> types to change the shape of how the data is returned.</p>
<p>We can then store the result as a Python dictionary and insert it into our new model. We can also log a JSON representation of the instance to make debugging easier. See <a href="https://django-ninja.dev/guides/response/#serializing-outside-of-views">Serializing Outside of Views</a> for an overview of how the <code>from_orm</code> API works.</p>
<p>To test this, I built a proof of concept Django management command using <a href="https://github.com/GaretJax/django-click">django-click</a>, which loops through all our legacy category models and prints them.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># management/commands/demo_model_schema.py</span>
<span style="color:#f92672">import</span> djclick <span style="color:#66d9ef">as</span> click
<span style="color:#f92672">from</span> ninja <span style="color:#f92672">import</span> ModelSchema
<span style="color:#f92672">from</span> pydantic <span style="color:#f92672">import</span> Field
<span style="color:#f92672">from</span> legacy.models <span style="color:#f92672">import</span> LegacyCategory
<span style="color:#f92672">from</span> future.models <span style="color:#f92672">import</span> Category
<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LegacyCategorySchema</span>(ModelSchema):
name: str <span style="color:#f92672">=</span> Field(alias<span style="color:#f92672">=</span><span style="color:#e6db74">"cat_name"</span>)
description: str <span style="color:#f92672">=</span> Field(alias<span style="color:#f92672">=</span><span style="color:#e6db74">"cat_description"</span>)
active: bool <span style="color:#f92672">=</span> Field(alias<span style="color:#f92672">=</span><span style="color:#e6db74">"cat_is_active"</span>)
<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Meta</span>:
fields <span style="color:#f92672">=</span> [<span style="color:#e6db74">"id"</span>]
model <span style="color:#f92672">=</span> Category
<span style="color:#a6e22e">@click</span><span style="color:#f92672">.</span>command()
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">main</span>():
categories <span style="color:#f92672">=</span> LegacyCategory<span style="color:#f92672">.</span>objects<span style="color:#f92672">.</span>all()
<span style="color:#66d9ef">for</span> category <span style="color:#f92672">in</span> categories:
data <span style="color:#f92672">=</span> LegacyCategorySchema<span style="color:#f92672">.</span>from_orm(category)<span style="color:#f92672">.</span>dict()
print(data)
<span style="color:#75715e"># save to a database or do something useful here</span>
</code></pre></div><h2 id="more-resources">More resources</h2>
<p>If you are curious about what Django Ninja is about, I recommend starting with their <a href="https://django-ninja.dev/tutorial/other/crud/#final-code">CRUD example: Final Code</a>, and working backward. This will give you a good idea of what a finished CRUD Rest API looks like with Django Ninja.</p>
Upgrading Python from 3.11 to 3.12 notes
https://micro.webology.dev/2024/03/05/upgrading-python-from-to-notes/
Tue, 05 Mar 2024 09:15:40 -0600http://webology.micro.blog/2024/03/05/upgrading-python-from-to-notes/<p>Recently, I have been slowly moving several of my side projects and client projects from various Python versions to Python 3.12.</p>
<p>I never see people write about this, so it might be nice to write and share some notes.</p>
<h2 id="where-to-start">Where to start</h2>
<p>The first thing we do with a relatively simple upgrade is figure out what Python version we use. Thankfully, the project we picked mentioned in the README.md that it was using Python 3.11.</p>
<p>Once we know which version of Python we are using, we can open up iTerm and get a git checkout of the project.</p>
<p>Next, we will run <code>git grep 11</code>, where “11” is the shortened form of the Python version that we are running. There are so many variations of <code>3.11</code> and <code>311</code> that using the minor version tends to be about right.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-bash" data-lang="bash">$ git grep <span style="color:#ae81ff">11</span>
... really long list...
.github/workflows/actions.yml: - name: Set up Python 3.11
.github/workflows/actions.yml: python-version: <span style="color:#e6db74">'3.11'</span>
.github/workflows/actions.yml: - name: Set up Python 3.11
.github/workflows/actions.yml: python-version: <span style="color:#e6db74">'3.11'</span>
.pre-commit-config.yaml: python: python311
.pre-commit-config.yaml: args: <span style="color:#f92672">[</span>--py311-plus<span style="color:#f92672">]</span>
README.md:This project will use Python 3.11, Docker, and Docker Compose.
README.md:Make a Python 3.11.x virtualenv.
docker/Dockerfile:FROM python:3.11-slim as builder-py
docker/Dockerfile:FROM python:3.11-slim AS release
pyproject.toml:requires-python <span style="color:#f92672">=</span> <span style="color:#e6db74">">= 3.11"</span>
pyproject.toml:# Assume Python ><span style="color:#f92672">=</span>3.11.
pyproject.toml:target-version <span style="color:#f92672">=</span> <span style="color:#e6db74">"py311"</span>
requirements.txt:# This file is autogenerated by pip-compile with Python 3.11
... lots and lots of files...
</code></pre></div><p>This output will give us a long list of files. Usually, this is 100s or 1000s of files we will pipe or copy into our code editor. We will make a few passes to remove all of the CSS, SVG, and HTML files in the list, and that pairs down the results to half a dozen or a dozen files.</p>
<h2 id="create-a-new-git-branch">Create a new git branch</h2>
<p>Next, we will create a new git branch called <code>upgrade-to-python-3.12</code>, and we will open each file one by one, and replace every “3.11” and “311” reference with “3.12” and “312” respectively.</p>
<h2 id="lintformat-our-code-base">Lint/format our code base</h2>
<p>Once we have all of our files updated, we will commit everything. Then we will note special files like <code>.pre-commit-config.yaml</code> and <code>pyproject.toml</code>, impacting how my Python files are linted and formatted. Then, we will run <code>pre-commit</code> immediately after and commit any formatting changes.</p>
<h2 id="rebuild-our-docker-image">Rebuild our Docker image</h2>
<p>Since this project contains <code>docker/Dockerfile</code> that tells us the project uses Docker, we will need to rebuild our container image and note anything that breaks.</p>
<h2 id="re-pinfreeze-our-python-dependencies">Re-pin/freeze our Python dependencies</h2>
<p>Next, we will run <code>pip-tools</code> <code>compile</code> from within our newly rebuilt Docker container to build a new <code>requirements.txt</code> using Python 3.12.</p>
<h2 id="re-rebuild-our-docker-image-again">Re-rebuild our Docker image (again)</h2>
<p><strong>“Insert Xzibit Yo Dawg meme."</strong> Next, we rebuild our Docker image using the newly pinned <code>requirements.txt</code> file, and this should be our final image.</p>
<h2 id="did-our-tests-pass">Did our tests pass?</h2>
<p>Assuming Docker builds cleanly, we will run my test suite using <code>pytest</code>. Once our tests pass, we’ll commit any uncommitted changes, <code>git push</code> our branch to GitHub, and open a Pull Request for review.</p>
<h2 id="did-our-tests-pass-in-ci">Did our tests pass in CI?</h2>
<p>If our tests pass on GitHub Actions in CI, then we know our upgrade was successful, and we are reasonably confident that</p>
<h2 id="when-things-dont-just-work">When things don’t “just work.”</h2>
<p>If you keep up with your upgrades, most of the time, everything works. Half a dozen projects did work for me, but I had one that did not work on Monday. There was a sub-dependency issue, so I closed my branch and opened a new issue to revisit this upgrade once the next version of Python 3.12.3 is released.</p>
<p>Even though this wasn’t a Python 3.12.2 bug, it takes the Python ecosystem time to catch up with newer versions. Since Python 3.11 is still supported for another <a href="https://endoflife.date/python">3 years and 7 months</a> (as of this writing), it won’t hurt to wait a few weeks or months and revisit these changes.</p>
<p>If you are curious about how I decide when to adopt a new version, I wrote about that last month: <a href="https://micro.webology.dev/2024/02/01/choosing-the-right.html">Choosing the Right Python and Django Versions for Your Projects</a></p>
On pip isolation
https://micro.webology.dev/2024/03/04/on-pip-isolation/
Mon, 04 Mar 2024 08:50:39 -0600http://webology.micro.blog/2024/03/04/on-pip-isolation/<p>I saw this <a href="https://mastodon.social/@treyhunner/112032637878747686">post</a> by <a href="https://mastodon.social/@treyhunner">Trey Hunner</a> about pip isolation, and I wanted to share a third method.</p>
<blockquote>
<p>I’ve just updated my ~/.config/pip/pip.conf & my dotfiles repo to disallow pip installing outside virtual environments! ๐</p>
<p>TIL 2 things about #Python’s pip:</p>
<ol>
<li>
<p>pip has a config file. If I ever knew this, I’d forgotten.</p>
</li>
<li>
<p>pip has an option that stops it from working outside of a virtual environment!</p>
</li>
</ol>
<p><a href="https://mastodon.social/@treyhunner/112032637878747686">https://mastodon.social/@treyhunner/112032637878747686</a></p>
</blockquote>
<p>To Trey’s point, I never pip to install to easily install anything globally. If I want something installed globally, I can jump through a few hoops to avoid polluting my global pip cache.</p>
<p>My preferred way of disallowing pip installation outside virtual environments is to use the <code>PIP_REQUIRE_VIRTUALENV</code> environment variable.</p>
<p>I have <code>export PIP_REQUIRE_VIRTUALENV=true</code> set in my <code>.bash_profile</code>, which is part of my <a href="https://github.com/jefftriplett/dotfiles"><code>dotfiles</code></a>. I prefer the ENV approach because I share my files over many computers, and it’s one less file to keep up with.</p>
<p>When I want to <code>pip install</code> something globally, I use <a href="https://pipx.pypa.io/stable/"><code>pipx</code></a>, which installs each Python application into its isolated environment.</p>
<p>For the few times that I do need to install a Python application globally, I use:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">PIP_REQUIRE_VIRTUALENV<span style="color:#f92672">=</span>false python -m pip install <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span> --upgrade <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span> pip <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span> pipx
</code></pre></div><p>I have this recipe baked into my <a href="https://github.com/jefftriplett/dotfiles/blob/f59e2884daace42be404c1f028ea7312fed0bab2/home/justfile#L159-L168">global justfile</a> so I can quickly apply upgrades.</p>
New `django-startproject` update
https://micro.webology.dev/2024/03/02/new-djangostartproject-update/
Sat, 02 Mar 2024 22:27:42 -0600http://webology.micro.blog/2024/03/02/new-djangostartproject-update/<p>I updated my <a href="https://github.com/jefftriplett/django-startproject"><code>django-startproject</code></a> project today to support the latest versions of Django, Python, Compose, and other tools I’m a fan of. I use <code>django-startproject</code> to spin up projects that need some batteries quickly, but not every battery.</p>
<p>Features:</p>
<ul>
<li>Django 5.0</li>
<li>Python 3.12</li>
<li>Docker Compose 3</li>
<li>Adds <a href="https://github.com/casey/just">casey/just</a> recipes/workflows (Just is a command runner, not a build tool)</li>
<li>Adds <a href="https://github.com/astral-sh/uv">uv</a> support</li>
</ul>
<p><code>uv</code> is the newest addition, which is a Python package installer and <a href="https://github.com/jazzband/pip-tools/">pip-tools</a> replacement. It’s not a 100% drop-in replacement for pip and pip-tools, but it cuts my build times in half, and I have yet to hit any significant show-stoppers.</p>
We did it, and what's next
https://micro.webology.dev/2024/03/01/we-did-it-and-whats/
Fri, 01 Mar 2024 20:07:26 -0600http://webology.micro.blog/2024/03/01/we-did-it-and-whats/<p>My February goal was to publish an article every day for the entire month, and I’m happy to have completed it.</p>
<p>Shout out to the wonderful <a href="https://wiggle.work">Wiggle Work</a> community who started this Winter Writing Workshop. As someone who prides themself on never succumbing to peer pressure, the joke is on me! I finished and only used my <strong>phone a friend</strong> lifeline once.</p>
<p>Shoutout to my Wiggle Work friends, and thank you to everyone who joined this month and everyone who cheered us on from the Discord. Your emojis of support were the true friends we made along the way.</p>
<p>The other four who finished:</p>
<ul>
<li><a href="https://www.breakfastletters.com/archives">Joni</a></li>
<li><a href="https://garden.mattstein.com/notes">Matt</a></li>
<li><a href="https://nicksimson.com/posts/">Nick</a></li>
<li><a href="https://www.sarahgebauer.com/post/">Sarah</a></li>
</ul>
<h2 id="writing-tools">Writing tools</h2>
<p>Every year, I put “write more” on my list of goals, and every year, I look back and realize some invisible barrier gets in my way. I get easily frustrated with every writing tool I have ever used, and my brain obsesses over the platform’s limitations.</p>
<p>I had a little bit of new writing tool productivity with both <a href="https://obsidian.md">Obsidian</a> and <a href="https://micro.blog">Micro.blog</a> which were both invaluable to me.</p>
<p>Writing every day in February instilled in me that Obsidian is the best writing platform for me. I even paid for a <a href="https://help.obsidian.md/Licenses+and+payment/Catalyst+license">Catalyst license</a> to support Obsidian’s development.</p>
<p><a href="https://micro.blog">Micro.blog</a> also made it easy to post every day, and I suspect it will shape and heavily influence my blog platform going forward. I’m still annoyed that I can’t upload images or edit my social media posts with it, but once I understood the service’s limitations, it never got in my way.</p>
<h2 id="going-forward">Going forward</h2>
<p>My existing blog uses GitHub Pages and Jekyll, but I’m ready to return to Django with a basic REST API for posting new blog posts and links.</p>
<p>My existing blog is built with Frontmatter, Markdown, and Yaml, which makes importing and editing everything doable with Python and a text editor.</p>
<p>I plan to use a mix of Obsidian with a custom Python script that can publish and sync to my website until I find a nice JS writing interface that I like.</p>
<p>Now that Obsidian supports <a href="https://help.obsidian.md/Editing+and+formatting/Properties">Properties</a> via Frontmatter, I can store my Post metadata between my text files and my blog to sync between the two.</p>
<p>I also want a slick <a href="https://typefully.com">Typefully</a>-like experience for quick posts. Just one big markdown-friendly writing area that can handle dragging and dropping images to it. I want to pick where I am publishing when I choose to publish.</p>
<p>That’s where I plan to go next.</p>
Python's UV tool is actually pretty good
https://micro.webology.dev/2024/02/29/pythons-uv-tool-is-actually/
Thu, 29 Feb 2024 13:31:04 -0600http://webology.micro.blog/2024/02/29/pythons-uv-tool-is-actually/<p>I carved out some time recently to start playing with the new Python package installer and resolver, <a href="https://github.com/astral-sh/uv"><code>uv</code></a>.</p>
<p><code>uv</code> makes big promises and claims to be 10-100x faster than pip and pip-tools. From my experiments over the last few weeks, it lives up to this promise.</p>
<p>I’m using it locally for my virtual environments, in my Dockerfiles to rebuild my containers, and for CI using GitHub Actions. Across the board, anything I do with <code>pip</code> or <code>pip-tools</code> is remarkably faster.</p>
<p>My average GitHub Actions CI workflows dropped from ~2 minutes to 50 seconds. This cuts the minutes I use in half and, in theory, my monthly bill in half.</p>
<p>My goal in sharing my configs is more “show” than “tell' because I will copy and paste these for weeks and months to come.</p>
<h2 id="local-development">local development</h2>
<p>Every one of my projects has a <code>justfile</code> (it’s like Make but works the same everywhere) with “bootstrap” and “lock” recipes. My “bootstrap” recipe installs everything I need to work with the project locally. I use my “lock” recipe to lock my requirements.txt file so that I’m using the exact requirements locally and in production.</p>
<h3 id="justfile-before"><code>justfile</code> before</h3>
<p>My <code>justfile</code> might look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">@<span style="color:#ae81ff">bootstrap</span>
<span style="color:#ae81ff">python -m pip install --upgrade pip</span>
<span style="color:#ae81ff">python -m pip install --upgrade --requirement requirements.in</span>
<span style="color:#f92672">@lock *ARGS</span>:
<span style="color:#ae81ff">python -m piptools compile {{ ARGS }} ./requirements.in \</span>
--<span style="color:#ae81ff">resolver=backtracking \</span>
--<span style="color:#ae81ff">output-file ./requirements.txt</span>
</code></pre></div><h3 id="justfile-after"><code>justfile</code> after</h3>
<p>For the most part, <code>uv</code> shares most of the same syntax as <code>pip</code> so you can start by changing your <code>pip</code> references to <code>uv pip</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">@<span style="color:#ae81ff">bootstrap</span>
<span style="color:#ae81ff">python -m pip install --upgrade pip uv</span>
<span style="color:#ae81ff">python -m uv pip install --upgrade --requirement requirements.in</span>
<span style="color:#f92672">@lock *ARGS</span>:
<span style="color:#ae81ff">python -m uv pip compile {{ ARGS }} ./requirements.in \</span>
--<span style="color:#ae81ff">resolver=backtracking \</span>
--<span style="color:#ae81ff">output-file ./requirements.txt</span>
</code></pre></div><h2 id="dockerfiles">Dockerfiles</h2>
<p>Everyone’s container setup is going to be different, but I use <a href="https://www.docker.com">Docker</a> and <a href="https://orbstack.dev">Orbstack</a>, which use a Dockerfile.</p>
<h2 id="dockerfile-before"><code>Dockerfile</code> before</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-Dockerfile" data-lang="Dockerfile"><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> python:3.12-slim-bookworm</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PIP_DISABLE_PIP_VERSION_CHECK <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PYTHONDONTWRITEBYTECODE <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PYTHONPATH /srv<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PYTHONUNBUFFERED <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> apt-get update<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> pip install --upgrade pip<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> requirements.txt /src/requirements.txt<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> pip install --requirement /src/requirements.txt<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">WORKDIR</span><span style="color:#e6db74"> /src/</span><span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><h2 id="dockerfile-after"><code>Dockerfile</code> after</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-Dockerfile" data-lang="Dockerfile"><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> python:3.12-slim-bookworm</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PATH /venv/bin:$PATH. <span style="color:#75715e"># this is new</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PIP_DISABLE_PIP_VERSION_CHECK <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PYTHONDONTWRITEBYTECODE <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PYTHONPATH /srv<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENV</span> PYTHONUNBUFFERED <span style="color:#ae81ff">1</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> apt-get update<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> pip install --upgrade pip uv <span style="color:#75715e"># this is updated</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> python -m uv venv /venv <span style="color:#75715e"># this is new</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> requirements.txt /src/requirements.txt<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">RUN</span> uv pip install --requirement /src/requirements.txt <span style="color:#75715e"># this is updated</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">WORKDIR</span><span style="color:#e6db74"> /src/</span><span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><h2 id="github-actions">GitHub Actions</h2>
<p>GitHub Actions are a little harder to explain, but my workflows started off similar to this before I made the switch to <code>uv</code>:</p>
<h3 id="mainyml-before"><code>main.yml</code> before</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">
- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Set up Python 3.12</span>
<span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-python@v5</span>
<span style="color:#f92672">with</span>:
<span style="color:#f92672">python-version</span>: <span style="color:#e6db74">'3.12'</span>
- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Install dependencies</span>
<span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span><span style="color:#e6db74"> </span> <span style="color:#ae81ff">python -m pip install --requirement requirements.in</span>
- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Collect Static Assets</span>
<span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span><span style="color:#e6db74"> </span> <span style="color:#ae81ff">python -m manage collectstatic --noinput</span>
</code></pre></div><h3 id="mainyml-after"><code>main.yml</code> after</h3>
<p>The biggest pain point that I ran into along the way was related to GitHub Issue <a href="https://github.com/astral-sh/uv/issues/1386">#1386</a>, which has a useable workaround.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yaml" data-lang="yaml">
- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Set up Python 3.12</span>
<span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/setup-python@v5</span>
<span style="color:#f92672">with</span>:
<span style="color:#f92672">python-version</span>: <span style="color:#e6db74">'3.12'</span>
- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Install dependencies</span>
<span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span><span style="color:#e6db74"> python -m pip install uv
</span><span style="color:#e6db74"> python -m uv venv .venv
</span><span style="color:#e6db74"> echo "VIRTUAL_ENV=.venv" >> $GITHUB_ENV
</span><span style="color:#e6db74"> echo "$PWD/.venv/bin" >> $GITHUB_PATH
</span><span style="color:#e6db74"> python -m uv pip install --requirement requirements.in</span>
- <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Collect Static Assets</span>
<span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span><span style="color:#e6db74"> . .venv/bin/activate
</span><span style="color:#e6db74"> python -m manage collectstatic --noinput</span>
</code></pre></div><h2 id="conclusion">Conclusion</h2>
<p>I hope this was helpful to anyone who is considering making the switch to <code>uv</code>. I love to hear about how much time it saves you.</p>
<h2 id="updates">Updates</h2>
<p>2024-03-08 - I modified the ENV PATH statement to prepend instead of replacing the value.</p>
Using Django Q2
https://micro.webology.dev/2024/02/27/using-django-q/
Tue, 27 Feb 2024 22:23:33 -0600http://webology.micro.blog/2024/02/27/using-django-q/<p>I’m long overdue to write about how <a href="https://github.com/django-q2/django-q2">Django Q2</a> has become part of my development toolkit. As the maintained successor to <a href="https://github.com/Koed00/django-q">Django Q</a>, Django Q2 extends Django to handle background tasks and scheduled jobs.</p>
<p>Django Q2 is flexible in managing tasks, whether sending out daily emails or performing hourly tasks like checking RSS feeds. The project works seamlessly with Django, making it one of the more straightforward background task solutions to integrate into your projects.</p>
<p>Using Django Q2 involves passing a method or a string reference to a method to an <a href="https://django-q2.readthedocs.io/en/master/tasks.html#async-task"><code>async_task()</code></a> function, which will run in the background.</p>
<p>One feature of Django Q2 that particularly impresses me is its adaptability to various databases. Whether your project uses the default Django database or something more scalable like Redis, Django Q2 fits perfectly. This flexibility means that a database queue suffices without any hiccups for most of my projects, even those that are small to medium.</p>
<p>Unlike other task queues that require managing multiple processes or services, Django Q2 keeps it simple. The only necessity is to have the <code>qcluster</code> management command running, which is a breeze compared to other task queues because you only need to run one service to handle everything.</p>
<p>Django Q2’s flexibility, ease of use, and seamless integration with Django make it an excellent tool to reach for when you need background tasks.</p>
Things I do every time I start a Django project
https://micro.webology.dev/2024/02/26/things-i-do-every-time/
Mon, 26 Feb 2024 19:15:29 -0600http://webology.micro.blog/2024/02/26/things-i-do-every-time/<p>In the spirit of Brenton Cleeland’s <a href="https://brntn.me/blog/six-things-i-do-every-time-i-start-a-django-project/">Six things I do every time I start a Django project</a>, my goal is to document and share my process since this often comes up.</p>
<p>Like Brenton, I have a lot of projects, and their scope may range from quick one-offs that might last a few hours to projects I have worked on for a decade or more.</p>
<h2 id="overview-and-upfront-notes">Overview and upfront notes</h2>
<p>I use <a href="https://github.com/pyenv/pyenv">pyenv</a> to manage my Python versions, but I don’t go into any details here.</p>
<p>I use <a href="https://github.com/pyenv/pyenv-virtualenvwrapper">pyenv-virtualenvwrapper</a> to manage what Python calls a “virtual environment,” which copies and symlinks files and folders where they need to be so that your Python projects are isolated from one another.</p>
<p><code>pyenv-virtualenvwrapper</code> is also a <code>pyenv</code>-friendly fork of <a href="https://github.com/python-virtualenvwrapper/virtualenvwrapper">virtualenvwrapper</a>.</p>
<p><strong>Please note:</strong> To avoid even more confusion, please assume that at any time in this article, when I mention <code>virtualenvwrapper</code> or its various commands, I mean the <code>pyenv-virtualenvwrapper</code> version of these tools.</p>
<p>For more detail, see my article <a href="https://micro.webology.dev/2024/02/10/python-development-on.html">Python Development on macOS Notes: pyenv and pyenv-virtualenvwrapper</a>.</p>
<h2 id="creating-a-new-project">Creating a new project</h2>
<p>First, I need a folder to store all my project files. One nice feature of <code>virtualenvwrapper</code> is it can manage both my virtualenvs and where my projects live.</p>
<p>So my first step for a new project is to run <code>mkvirtualenv {project_name}</code>, where <code>project_name</code> is the name of the domain name or client that I’m working with. <code>mkvirtualenv</code> will then create a new virtual environment (<code>virtualenv</code> or <code>venv</code> is fine too) for me, make a new project folder for me to put my files into, and then transport me to this new project folder.</p>
<p><code>virtualenvwrapper</code> also supports custom hooks, which is a fancy way of saying it runs a series of commands I tell it I want to run after it creates a <code>virtualenv</code>. These are a few of the custom steps that I run for my projects:</p>
<ul>
<li>Create a <a href="https://www.sublimetext.com/blog/articles/sublime-text-4">Sublime Text</a> project file so <a href="https://www.alfredapp.com/">Alfred</a> can detect it.</li>
<li>Create a <a href="https://www.sublimetext.com/blog/articles/sublime-text-4">Sublime Text</a> <code>pyrightconfig.json</code> file, which enables LSP support for the project so that my code autocompletion works.</li>
<li>Create a <a href="https://syncthing.net">Syncthing</a> <code>.stignore</code> file.</li>
<li>Create a <a href="https://direnv.net">direnv</a> <code>.envrc</code> file that loads my project’s environment variables.</li>
<li>Transports me into my projects folder.</li>
<li>Activate the new <code>virtualenv</code>, so Python is ready.</li>
<li>Install the latest version of <code>pip</code>, Python’s package manager.</li>
<li>Install the latest version of <a href="https://github.com/jazzband/pip-tools/">pip-tools</a>, which I like to use for managing my project Python requirements.</li>
<li><strong>Soon:</strong> Install the latest version of <a href="https://github.com/astral-sh/uv">uv</a>, which will soon replace pip-tools for me.</li>
</ul>
<p>Even though most of my projects use Docker Compose, I still install my Python dependencies outside of Docker in the virtual environment so that my text editors and <a href="https://github.com/microsoft/pyright">pyright</a> can read them.</p>
<h2 id="create-my-django-app">Create my Django app</h2>
<p>Once I have a project folder and <code>virtualenv</code> set up, I must create a Django project.
I use either my <a href="https://github.com/jefftriplett/django-startproject">django-startproject</a> or <a href="https://revsys.com">REVSYS</a>’s Django starter template, which we call AlphaKit.
Both projects aim to start jumpstart development and spend zero time configuring projects.
Since my stater project is public, I will run this snippet to jumpstart it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">django-admin startproject <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span> --extension<span style="color:#f92672">=</span>ini,py,yml <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span> --template<span style="color:#f92672">=</span>https://github.com/jefftriplett/django-startproject/archive/main.zip <span style="color:#ae81ff">\
</span><span style="color:#ae81ff"></span> config
</code></pre></div><p>I landed on <code>config</code> for my projects because I like consistency when switching between projects, and over a decade and a half, I have never had a naming collision with config. The name config tends to sort to the top of my folder listings, making copying and pasting configs from project to project easier.</p>
<h2 id="setup-git">Setup Git</h2>
<ol>
<li>Now that I have my project started, I will run <code>git init</code> inside the project folder.</li>
<li><code>bunx gitignore python</code> to set standard files to ignore</li>
<li>I will create a new repo on Git(Hub|Lab) and set it as a remote source for my project</li>
<li>I will run <code>gittower .</code>, select which files I’m ready to commit, and write my initial commit message.</li>
<li>I will <code>git push</code> from my terminal or <a href="https://www.git-tower.com/mac">GitTower</a> and confirm that my new project is set up.</li>
</ol>
<h2 id="switching-projects">Switching projects</h2>
<p>When I want to work on an existing project, I use <code>virtualenv</code>’s <code>workon</code> command to switch projects. <code>workon {project_name}</code> will transport me to the correct project folder and will activate my Python virtual environment so that I’m ready to work.</p>
<h2 id="do-i-need-templates">Do I need templates?</h2>
<p>If my project needs templates, I’ll use a starter <code>templates/base.html</code> and the <a href="https://tailwindcss.com/docs/installation/play-cdn">Tailwind CSS Play CDN</a>.</p>
<p>I am a few years into using Tailwind CSS, and it’s my first choice for quickly bootstrapping a project. Despite the masto-hate, Tailwind CSS works for me and is vanilla CSS-friendly.</p>
<p>I like their Play CDN because I can start fiddling with Tailwind CSS with zero runtime and only one script tag to include in my HTML docs head.</p>
Weeknotes 2024 W08
https://micro.webology.dev/2024/02/25/weeknotes-w/
Sun, 25 Feb 2024 19:42:20 -0600http://webology.micro.blog/2024/02/25/weeknotes-w/<p>It warmed up this week, and we started our mornings out in the 30s and ended them in the 60s and even 70s all week. Our neighbors started putting their furniture back out on their porches, which is a local sign that winter is almost over.</p>
<h2 id="family">Family</h2>
<p>We found a small gas leak in our >100-year-old house and hired someone to fix it. If you ever smell gas, friends, please call and have it fixed as soon as possible.</p>
<p>I have been working with my son every day on his basketball skills. He missed his first game Saturday because of a stomach bug but bounced back enough Sunday to join me at the gym to practice. He went from zero dribbling skills to leading the team in dribbling drills, which he is pretty proud of.</p>
<p>My youngest child is firmly in the three to four-word sentence phase. My other half returned to work this week after being off for a procedure.</p>
<h2 id="work">Work</h2>
<p>I started a new client project this week, which involved spinning up a new green field project, taking a bunch of CSV files, and importing millions of records. The goal is to use Django, HTMX, and a lot of neat tech to build a vast parts catalog.</p>
<h2 id="community">Community</h2>
<p>We met to discuss our <a href="https://2024.djangocon.us">DjangoCon US</a> website options as we try to figure out what we want in a new platform. We had a follow-up meeting about our Health and Safety policies, and I also had a meeting to help onboard someone to assist with our automation projects.</p>
<p>I joined the <a href="https://www.conferencechats.org">Conference Chats</a> meetup this week and spent a few hours discussing conferences with other community organizers.</p>
<p>I joined two <a href="https://wiggle.work">Wiggle Work</a> events this week over lunch with the same group that had been writing this month.</p>
<p>I hosted <a href="https://mastodon.social/@webology/111983160101281972">Office Hours</a> again this week.</p>
<h2 id="side-projects">Side Projects</h2>
<p>I spent most of my side project time on <a href="https://django-news.com">Django News</a>, <a href="https://jobs.django-news.com">Django News Jobs</a>, and <a href="https://djangopackages.org">Django Packages</a>.</p>
<p><strong>Django News Jobs:</strong> I fixed several bugs, styled some message notifications, and refactored how various job status states work. We have different workflows for when a human submits a job versus jobs we collect from RSS feeds, APIs, or scraped. Most of these jobs are collected but have yet to be displayed. They should start seeing the light of day once I get a better handle on a moderation queue.</p>
<p><strong>Django Packages:</strong> The website now runs on <a href="https://github.com/djangopackages/djangopackages/pull/1105">Django 5.0</a> thanks to <a href="https://github.com/saadmk11">Maksudul Haque</a>. I merged in several rounds of dependency updates and pre-commit updates.</p>
<p><strong>Django News Newsletter:</strong> We shipped issue <a href="https://django-news.com/issues/220#start">#220</a>. I worked on our secret backend project, which helps us collect news and makes updating the newsletter easier. I migrated our Tweet model to a Drafts model and dusted off some code that would post via the <a href="https://typefully.com">Typefully</a> API.</p>
<p>All three projects received security updates, and I created a new blog app called the Changelog, which we’ll slowly roll out over the next few weeks or months. That should help us blog about project updates and release and share thoughts.</p>
<p>I started using the <a href="https://astral.sh/blog/uv"><code>uv</code></a> project to shave a minute or more off local updates and some Docker rebuilds. I’m trying it out for a week and then rolling it out.</p>
<h2 id="me-time">“Me” time</h2>
<p>I finished <a href="https://trakt.tv/shows/true-detective/seasons/4">True Detective: Season 4: Night Country</a></p>
<p>I watched the first two seasons of <a href="https://trakt.tv/shows/resident-alien">Resident Alien</a></p>
<p>I watched two Kansas Basketball games and a bunch of random games.</p>
<p>I wrote seven blog posts, part of my goal to blog daily in February with the <a href="https://wiggle.work">Wiggle Work</a> Discord community I’m part of.</p>
<p><a href="https://micro.webology.dev/2024/02/24/fetch-the-contents.html">2024-02-24</a>:ย <strong>Fetch the contents of a URL with Django service</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/02/23/three-years-later.html">2024-02-23</a>:ย <strong>Three years later, email is less of a pain</strong></p>
<p><a href="https://micro.webology.dev/2024/02/22/scratching-itches-with.html">2024-02-22</a>:ย <strong>Scratching Itches with Python and ChatGPT</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/02/21/using-chamber-with.html">2024-02-21</a>:ย <strong>Using Chamber with Django and managing environment variables</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/02/20/transferred-djangorqemailbackend-found.html">2024-02-20</a>:ย <strong>Transferred - django-rq-email-backend found a new home</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/02/19/django-ninja.html">2024-02-19</a>:ย <strong>๐ฅท Django Ninja</strong>ย </p>
<p><a href="https://micro.webology.dev/2024/02/18/my-development-setup.html">2024-02-18</a>:ย <strong>My 2024 Development Setup</strong>ย </p>
<h2 id="next-week">Next week</h2>
<p>I’ll finally take down those Christmas lights. I must repair rot damage on the front porch and our back steps. It’s almost time to start reseeding our yard and removing the extra sticks that have accumulated over the winter.</p>
Fetch the contents of a URL with Django service
https://micro.webology.dev/2024/02/24/fetch-the-contents-of-a/
Sat, 24 Feb 2024 14:21:43 -0600http://webology.micro.blog/2024/02/24/fetch-the-contents-of-a/<p>For the last few months, I have been using the <a href="https://cooked.wiki">cooked.wiki</a> recipe-saving website, which initially impressed me because of how easy the website’s API is to use.</p>
<p>To use the service, all one has to do is prepend any website that contains a food recipe with <code>https://cooked.wiki/</code>, and you get the recipe without a coming-of-age discovery story.</p>
<p>This is a fun pattern, so I wrote my own in Django to illustrate how to build a Django view, which accepts a URL like http://localhost:8000/https://httpbin.org/get/ where <a href="https://httpbin.org/get/">https://httpbin.org/get/</a> will be fetched and the contents stored for processing.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># views.py </span>
<span style="color:#f92672">import</span> httpx
<span style="color:#f92672">from</span> django.http <span style="color:#f92672">import</span> HttpResponse
<span style="color:#f92672">from</span> urllib.parse <span style="color:#f92672">import</span> urlparse
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">fetch_content_view</span>(request, url: str) <span style="color:#f92672">-></span> HttpResponse:
<span style="color:#75715e"># Ensure the URL starts with http:// or https://</span>
parsed_url <span style="color:#f92672">=</span> urlparse(url)
<span style="color:#66d9ef">if</span> parsed_url<span style="color:#f92672">.</span>scheme <span style="color:#f92672">in</span> (<span style="color:#e6db74">"http"</span>, <span style="color:#e6db74">"https"</span>):
<span style="color:#66d9ef">try</span>:
response <span style="color:#f92672">=</span> httpx<span style="color:#f92672">.</span>get(url)
<span style="color:#75715e"># Check for HTTP request errors</span>
httpx<span style="color:#f92672">.</span>raise_for_status()
content <span style="color:#f92672">=</span> httpx<span style="color:#f92672">.</span>content
<span style="color:#75715e"># TODO: do something with content here...</span>
<span style="color:#66d9ef">assert</span> content
<span style="color:#66d9ef">return</span> HttpResponse(<span style="color:#e6db74">f</span><span style="color:#e6db74">"</span><span style="color:#e6db74">{</span>url<span style="color:#e6db74">=}</span><span style="color:#e6db74">"</span>)
<span style="color:#66d9ef">except</span> httpx<span style="color:#f92672">.</span>RequestException <span style="color:#66d9ef">as</span> e:
<span style="color:#66d9ef">return</span> HttpResponse(<span style="color:#e6db74">f</span><span style="color:#e6db74">"Error fetching the requested URL: </span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">}</span><span style="color:#e6db74">"</span>, status<span style="color:#f92672">=</span><span style="color:#ae81ff">500</span>)
<span style="color:#66d9ef">else</span>:
<span style="color:#66d9ef">return</span> HttpResponse(<span style="color:#e6db74">"Invalid URL format."</span>, status<span style="color:#f92672">=</span><span style="color:#ae81ff">400</span>)
</code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># urls.py</span>
<span style="color:#f92672">from</span> django.urls <span style="color:#f92672">import</span> path
<span style="color:#f92672">from</span> . <span style="color:#f92672">import</span> views
urlpatterns <span style="color:#f92672">=</span> [
<span style="color:#75715e"># other URL patterns here...</span>
<span style="color:#f92672">...</span>
path(<span style="color:#e6db74">"<path:url>/"</span>, views<span style="color:#f92672">.</span>fetch_content_view, name<span style="color:#f92672">=</span><span style="color:#e6db74">"fetch_content"</span>),
]
</code></pre></div><p>If you create your fetch the contents of a URL-like service, please consider putting it behind authentication to avoid someone discovering it and using it to DDOS someone’s website. I recommend throttling the view to prevent overloading a website by spamming requests to it.</p>
<p><strong>Updated:</strong> I updated the example to switch from the python-requests references to the <a href="https://www.python-httpx.org/compatibility/">HTTPX</a> library.</p>
Scratching Itches with Python and ChatGPT
https://micro.webology.dev/2024/02/22/scratching-itches-with-python-and/
Thu, 22 Feb 2024 19:56:38 -0600http://webology.micro.blog/2024/02/22/scratching-itches-with-python-and/<p>A few times a week over the last several months, I have paired with ChatGPT to work on Python scripts that solve problems that I would otherwise have spent less time on. Some might feel too niche or even too tedious that I would otherwise not take the time to work on. Most of the time, these are scratching an itch and solving a problem on my mind.</p>
<p>Because of the time constraint, I have been impressed with the results. I usually spend 10 to 15 minutes prompting ChatGPT, then I spend 10 to 15 minutes refactoring the script, adding <a href="https://typer.tiangolo.com">Typer</a>, and refining the code. Sometimes, this involved copying all or parts of my script and pasting it back into ChatGPT to have it refine or refactor some section of code.</p>
<h2 id="youtube-playlist-to-markdown-file">YouTube Playlist to Markdown file</h2>
<p>My <a href="https://gist.github.com/jefftriplett/4bf333453d7cab320aa45767b1949be6">YouTube playlist to markdown script</a> is helpful for quickly getting a list of video URLs and titles back from a YouTube playlist. I have used this for DjangoCon US and a few other conferences to help collect links for social media and a few times for the <a href="https://django-news.com">Django News Newsletter</a>.</p>
<p>ChatGPT even documented the process, including links for how to set permissions for the YouTube API.</p>
<h2 id="use-playwright-to-pull-data-out-of-the-django-admin">Use Playwright to pull data out of the Django Admin</h2>
<p>I have database access for most projects, but I recently needed to export a list of RSS feeds from the admin of a Django website. ChatGPT could quickly write a Playwright script to log in to the website, access a list page, and pull the feed field from the detail page. The script generated a JSON feed and could understand pagination and how to page through the links.</p>
<h2 id="github-issues-and-pull-request-templates">GitHub Issues and Pull Request templates</h2>
<p>For the <a href="https://github.com/wsvincent/awesome-django">Awesome Django project</a>, I asked ChatGPT to generate GitHub Issues and Pull Request templates based on my criteria. Once the templates were complete, I prompted ChatGPT to help me write a script that uses the GitHub API to read a pull request and validate the answers filled out while adding some other contextual data that makes it easier to verify the request.</p>
<h2 id="modeling-html-with-pydantic">Modeling HTML with PyDantic</h2>
<p>I am still trying to figure out what to do with the project, but I asked ChatGPT to use <a href="https://github.com/pydantic/pydantic">Pydantic</a> to create a class that could represent HTML tags. Once I was happy with the API, I asked ChatGPT to represent all HTML tags. After a few more prompts, I could read, write, and represent an HTML document using this script using Pydantic.</p>
<h2 id="outro">Outro</h2>
<p>I’m still determining how useful these scripts are, but I have enjoyed these quick sessions to write a one-off script or to solve problems that come up a few times a year that never seemed worth the time spent trying to write it from scratch.</p>
Using Chamber with Django and managing environment variables
https://micro.webology.dev/2024/02/21/using-chamber-with-django-and/
Wed, 21 Feb 2024 22:01:19 -0600http://webology.micro.blog/2024/02/21/using-chamber-with-django-and/<p>One of my favorite hosting setups is getting a cheap slice from Digital Ocean or your favorite provider, installing <a href="https://docs.docker.com/compose/">Docker Compose</a> and <a href="https://tailscale.com">Tailscale</a>, and then fire-walling everything off except for port 443.</p>
<p>Whenever I want to host a new project, I copy a <code>docker-compose.yml</code> file to the server, and then I start it with `docker compose up -d'.</p>
<p>I run <a href="https://github.com/containrrr/watchtower">Watchtower</a> in Docker on the server, which looks for new Docker images from GitHub Packages, pulls them, and restarts any updated containers.</p>
<p>I can update my projects, <code>git push</code> changes, and GitHub Actions will build and store a new container image for me.</p>
<p>My main pain point was juggling environment variables until someone pointed out <a href="https://github.com/segmentio/chamber">Chamber</a>, which manages environment variables well.</p>
<p>Since creating this setup, I have shared several GitHub Gists with curious friends, and my goal of this post is to serve as more of an overview of options than it is to be a comprehensive guide to using Chamber.</p>
<h2 id="prerequisites">Prerequisites</h2>
<p>You’ll need an AWS account, some essential Docker and Compose knowledge, and to follow <a href="https://github.com/segmentio/chamber#installing">Chamber’s Installing</a> instructions.</p>
<h2 id="setting-up-my-environment">Setting up my environment</h2>
<p>Ironically, my goal of eliminating individual environment variables led me to need four environment variables to bootstrap Chamber itself.
The environment variables I’m using:</p>
<ul>
<li><code>AWS_ACCESS_KEY_ID</code></li>
<li><code>AWS_REGION</code></li>
<li><code>AWS_SECRET_ACCESS_KEY</code></li>
<li><code>CHAMBER_KMS_KEY_ALIAS=aws/ssm</code></li>
</ul>
<h2 id="dockerfile-setup">Dockerfile Setup</h2>
<p>To make running Chamber running more straightforward, I used the <code>segment/chamber</code> Docker image, copied the <code>/bin/chamber</code> binary into my image, and configured it to run it as a <code>ENTRYPOINT</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-dockerfile" data-lang="dockerfile"><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> segment/chamber:2.14 AS chamber</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> python:3.11-slim-buster AS dev</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">FROM</span><span style="color:#e6db74"> dev AS production</span><span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span>...<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">COPY</span> --from<span style="color:#f92672">=</span>chamber /chamber /bin/chamber<span style="color:#960050;background-color:#1e0010">
</span><span style="color:#960050;background-color:#1e0010"></span><span style="color:#66d9ef">ENTRYPOINT</span> [<span style="color:#e6db74">"/bin/chamber"</span>, <span style="color:#e6db74">"exec"</span>, <span style="color:#e6db74">"django-news.com/production"</span>, <span style="color:#e6db74">"--"</span>]<span style="color:#960050;background-color:#1e0010">
</span></code></pre></div><p>I prefer to namespace these variables based on the project and the environment I’m referencing, like <code>django-news.com/production</code>.</p>
<p>I am using a Docker entrypoint so that my secrets/environment variables work by default, whether running the image or overriding the default command, so I may shell into my container.</p>
<h2 id="docker-compose-setup">Docker Compose Setup</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-yml" data-lang="yml"><span style="color:#f92672">services</span>:
<span style="color:#f92672">web</span>:
<span style="color:#f92672">entrypoint</span>: <span style="color:#ae81ff">/bin/chamber exec django-news.com/production --</span>
<span style="color:#f92672">command</span>: <span style="color:#ae81ff">gunicorn config.wsgi --bind 0.0.0.0:8000</span>
<span style="color:#ae81ff">...</span>
</code></pre></div><p>Please note that the <code>entrypoint</code> line is optional if you set it in your <code>ENTRYPOINT</code> setting in your <code>DOCKERFILE</code>.</p>
<h2 id="using-chamber">Using Chamber</h2>
<p>Now that you have seen how we use Chamber in Docker and Docker Compose, this is how we get things into Chamber.</p>
<h3 id="listing-our-projects">Listing our projects</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ chamber list-services
</code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ chamber list django-news.com/production
</code></pre></div><h3 id="write-new-settings">Write new settings</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ chamber write django-news.com/production DJANGO_DEBUG true
</code></pre></div><h3 id="delete-an-existing-setting">Delete an existing setting</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ chamber delete django-news.com/production DJANGO_DEBUG
</code></pre></div><h3 id="export-our-settings-into-a-dotenv-env-file">Export our settings into a dotenv (.env) file</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">$ chamber export --format<span style="color:#f92672">=</span>dotenv django-news.com/production
</code></pre></div><h3 id="consuming-an-env-variable-from-django">Consuming an env variable from Django</h3>
<p>The <a href="https://github.com/sloria/environs"><code>environs</code></a> project is my go-to for parsing environment variables. Here is an example of how to toggle Django’s debug mode.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># settings.py</span>
<span style="color:#f92672">import</span> environs
env <span style="color:#f92672">=</span> environs<span style="color:#f92672">.</span>Env()
DEBUG <span style="color:#f92672">=</span> env<span style="color:#f92672">.</span>bool(<span style="color:#e6db74">"DJANGO_DEBUG"</span>, default<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</code></pre></div><h2 id="conclusion">Conclusion</h2>
<p>I’m happy to manage my environment variables from the command line without syncing files.
Using Chamber with KMS increased my monthly AWS bill by $0.01, which is money well spent for the flexibility of using Chamber.</p>
<h2 id="alternatives">Alternatives</h2>
<p>I had a good experience using the 1Password CLI for a recent client project to share and load secrets into the environment.
If you are working with a team, consider checking it out for your team in case it’s a good fit.
Check out their <a href="https://developer.1password.com/docs/cli/secrets-environment-variables/">Load secrets into the environment</a> docs.</p>
๐ฅท Django Ninja
https://micro.webology.dev/2024/02/19/django-ninja/
Mon, 19 Feb 2024 16:58:37 -0600http://webology.micro.blog/2024/02/19/django-ninja/<p>I have heard good things about <a href="https://django-ninja.dev">Django Ninja</a>, and I was looking for an excuse to try it out.</p>
<p>One pain point is syncing production data to a local Django development website. Most of the time, I can dump a database and restore it locally. Sometimes, creating a REST endpoint and creating a local Django management command to consume that input is easier.</p>
<p>That’s what my example is doing here with Django Ninja.</p>
<h2 id="schemaspy"><code>schemas.py</code></h2>
<p>If you have used <a href="https://www.django-rest-framework.org">Django REST framework</a>’s <a href="https://www.django-rest-framework.org/api-guide/serializers/#modelserializer">ModelSerializer</a> before, you will feel at home with Django Ninja’s <a href="https://django-ninja.dev/guides/response/django-pydantic/#modelschema">ModelSchema</a>.</p>
<p>A ModelSchema class can generate a schema from a Django model and only needs to know which Django model and which fields you want to be exposed to your endpoint.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#f92672">from</span> ninja <span style="color:#f92672">import</span> ModelSchema
<span style="color:#f92672">from</span> places.models <span style="color:#f92672">import</span> Place
<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">PlaceSchema</span>(ModelSchema):
<span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Meta</span>:
model <span style="color:#f92672">=</span> Place
fields <span style="color:#f92672">=</span> [
<span style="color:#e6db74">"name"</span>,
<span style="color:#e6db74">"slug"</span>,
<span style="color:#e6db74">"active"</span>,
<span style="color:#e6db74">"place_id"</span>,
<span style="color:#e6db74">"address"</span>,
<span style="color:#f92672">...</span>
]
</code></pre></div><h2 id="viewspy"><code>views.py</code></h2>
<p>Our Django Ninja view is going to going to query our database and return all of the active records in our <code>Place</code> model with respect to their creation date. Django Ninja will do the heavy lifting for us and apply our <code>PlaceSchema</code> which will return JSON data.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#f92672">from</span> ninja <span style="color:#f92672">import</span> Router
<span style="color:#f92672">from</span> ninja.pagination <span style="color:#f92672">import</span> LimitOffsetPagination
<span style="color:#f92672">from</span> ninja.pagination <span style="color:#f92672">import</span> paginate
<span style="color:#f92672">from</span> places.models <span style="color:#f92672">import</span> Place
<span style="color:#f92672">from</span> places.schemas <span style="color:#f92672">import</span> PlaceSchema
router <span style="color:#f92672">=</span> Router()
<span style="color:#a6e22e">@router</span><span style="color:#f92672">.</span>get(<span style="color:#e6db74">"/places/"</span>, response<span style="color:#f92672">=</span>{<span style="color:#ae81ff">200</span>: list[PlaceSchema]})
<span style="color:#a6e22e">@paginate</span>(LimitOffsetPagination)
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">list_places</span>(request):
qs <span style="color:#f92672">=</span> Place<span style="color:#f92672">.</span>objects<span style="color:#f92672">.</span>active()<span style="color:#f92672">.</span>order_by(<span style="color:#e6db74">"-created"</span>)
<span style="color:#66d9ef">return</span> qs
</code></pre></div><h2 id="routerspy"><code>routers.py</code></h2>
<p>This <code>routers.py</code> file is optional, but I prefer to keep my routers separate from my <code>urls.py</code>, and this felt cleaner. I would keep this code for a small app in my <code>urls.py</code> instead.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#f92672">from</span> ninja <span style="color:#f92672">import</span> NinjaAPI
<span style="color:#f92672">from</span> places.views <span style="color:#f92672">import</span> router <span style="color:#66d9ef">as</span> places_router
<span style="color:#75715e"># API setup</span>
api <span style="color:#f92672">=</span> NinjaAPI(csrf<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>, version<span style="color:#f92672">=</span><span style="color:#e6db74">"1.0.0"</span>)
api<span style="color:#f92672">.</span>add_router(<span style="color:#e6db74">""</span>, places_router)
</code></pre></div><h2 id="urlspy"><code>urls.py</code></h2>
<p>Our <code>urls.py</code> file is where we expose our router to the Internet so that our <code>/apis/places/</code> URI will be accessible to anyone who knows it’s there.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#f92672">...</span>
<span style="color:#f92672">from</span> places.views <span style="color:#f92672">import</span> api
<span style="color:#f92672">...</span>
urlpatterns <span style="color:#f92672">=</span> [
<span style="color:#f92672">...</span>
path(<span style="color:#e6db74">"apis/"</span>, api<span style="color:#f92672">.</span>urls),
<span style="color:#f92672">...</span>
]
</code></pre></div><hr>
<h2 id="import_from_productionpy"><code>import_from_production.py</code></h2>
<p>This is a reasonably common Django management command pattern that I write. I prefer to use <a href="https://github.com/GaretJax/django-click">django-click</a> because the <a href="https://palletsprojects.com/p/click/">click</a> simplifies how to write Python command line interfaces.</p>
<p>We connect to our new endpoint, and if we get a good status code back, we will loop over our results and sync up our database with them.</p>
<p>Please note: I didn’t dive into how to do this security, but you will want to add some protection to your websites unless you are comfortable with it being public.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#f92672">import</span> djclick <span style="color:#66d9ef">as</span> click
<span style="color:#f92672">import</span> requests
<span style="color:#f92672">from</span> rich <span style="color:#f92672">import</span> print
<span style="color:#f92672">from</span> places.models <span style="color:#f92672">import</span> Place
<span style="color:#75715e"># Constants</span>
API_URL <span style="color:#f92672">=</span> <span style="color:#e6db74">"https://your-website.com/apis/places/"</span>
[<span style="color:#a6e22e">@click</span>](https:<span style="color:#f92672">//</span>micro<span style="color:#f92672">.</span>blog<span style="color:#f92672">/</span>click)()
<span style="color:#a6e22e">@click</span><span style="color:#f92672">.</span>option(<span style="color:#e6db74">"--limit"</span>, type<span style="color:#f92672">=</span>int, default<span style="color:#f92672">=</span><span style="color:#ae81ff">100</span>)
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">command</span>(limit):
<span style="color:#75715e"># Start a requests session</span>
<span style="color:#66d9ef">with</span> requests<span style="color:#f92672">.</span>Session() <span style="color:#66d9ef">as</span> session:
<span style="color:#66d9ef">try</span>:
params <span style="color:#f92672">=</span> {<span style="color:#e6db74">"limit"</span>: limit}
response <span style="color:#f92672">=</span> session<span style="color:#f92672">.</span>get(API_URL, params<span style="color:#f92672">=</span>params)
response<span style="color:#f92672">.</span>raise_for_status()
items <span style="color:#f92672">=</span> response<span style="color:#f92672">.</span>json()[<span style="color:#e6db74">"items"</span>]
<span style="color:#66d9ef">for</span> item <span style="color:#f92672">in</span> items:
<span style="color:#66d9ef">if</span> <span style="color:#e6db74">"name"</span> <span style="color:#f92672">in</span> item:
name <span style="color:#f92672">=</span> item[<span style="color:#e6db74">"name"</span>]
address <span style="color:#f92672">=</span> item[<span style="color:#e6db74">"address"</span>]
defaults <span style="color:#f92672">=</span> dict(item)
<span style="color:#75715e"># delete foreign keys </span>
<span style="color:#66d9ef">del</span> defaults[<span style="color:#e6db74">"cuisines"</span>]
<span style="color:#66d9ef">del</span> defaults[<span style="color:#e6db74">"neighborhood"</span>]
<span style="color:#66d9ef">try</span>:
place, created <span style="color:#f92672">=</span> Place<span style="color:#f92672">.</span>objects<span style="color:#f92672">.</span>update_or_create(
name<span style="color:#f92672">=</span>name, address<span style="color:#f92672">=</span>address, defaults<span style="color:#f92672">=</span>defaults
)
print(<span style="color:#e6db74">f</span><span style="color:#e6db74">"</span><span style="color:#e6db74">{</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74"> :: </span><span style="color:#e6db74">{</span>created<span style="color:#e6db74">=}</span><span style="color:#e6db74">"</span>)
<span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span> <span style="color:#66d9ef">as</span> e:
print(<span style="color:#e6db74">f</span><span style="color:#e6db74">"[red]</span><span style="color:#e6db74">{</span>e<span style="color:#e6db74">=}</span><span style="color:#e6db74">[/red]"</span>)
<span style="color:#66d9ef">except</span> requests<span style="color:#f92672">.</span>HTTPError <span style="color:#66d9ef">as</span> http_err:
print(<span style="color:#e6db74">f</span><span style="color:#e6db74">"HTTP error occurred: </span><span style="color:#e6db74">{</span>http_err<span style="color:#e6db74">}</span><span style="color:#e6db74">"</span>)
<span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span> <span style="color:#66d9ef">as</span> err:
print(<span style="color:#e6db74">f</span><span style="color:#e6db74">"An error occurred: </span><span style="color:#e6db74">{</span>err<span style="color:#e6db74">}</span><span style="color:#e6db74">"</span>)
</code></pre></div><h2 id="fin">Fin</h2>
<p>Overall, I’m pretty happy with this pattern and Django Ninja. My only nitpick is that I wish that Django Ninja shipped with some class-based view batteries. I find them to be much easier to extend and work from than function-based views, but I’m told some third-party apps are starting to help fill in this space.</p>
My 2024 Development Setup
https://micro.webology.dev/2024/02/18/my-development-setup/
Sun, 18 Feb 2024 21:39:06 -0600http://webology.micro.blog/2024/02/18/my-development-setup/<p>I have wanted to document my development setup for a while to revisit it from time to time to see how my setup changes.</p>
<h2 id="hardware">Hardware</h2>
<p>I’m typing this from my couch on a MacBook Pro Intel/2019 model in Lawrence, KS.</p>
<p>I’m using a Mac Studio (M2/2023 model) at my office.</p>
<p>I’m using a Mac Mini Pro (M2/2023 model) in my home office.</p>
<p>I’m using an Intel Skull NUC (2017) and several Raspberry Pis (3s and 4s) wired to my router.</p>
<p>After a few decades of working on a laptop, I recently switched to a desktop machine because I wanted to sit down at either machine and work without carrying anything around. With two kids, I’m also juggling backpacks, water bottles, and whatever they need at school or preschool that day. Not having to worry about carrying anything is a huge mental weight off my shoulders.</p>
<p>At home, I run three Dell S2722QC 27-inch 4K USB-C monitors and a 16" portable monitor that I use as a dedicated iTerm display. At work, I use a pair of Monoprice 30" monitors and another 16" portable monitor for iTerm. At some point, I want to pick up the same Dell monitors for work.</p>
<h2 id="software">Software</h2>
<p>Since 2007, I have preferred developing on macOS. I’m running on macOS Ventura on everything, but I plan to migrate to macOSย Sonoma sooner rather than later.</p>
<p>I use <a href="https://tailscale.com">Tailscale</a>, so I can connect to any of my machines from anywhere. I can even connect from my iPhone or an iPad. If you have more than one machine, even if that machine is a Raspberry Pi, Tailscale is worth the ~5-minute install.</p>
<p>I use <a href="https://www.alfredapp.com">Alfred</a>, with some custom plugins, as my go-to “do everything on macOS” tool of choice.</p>
<h2 id="themes">Themes</h2>
<p><a href="https://draculatheme.com/">Dracula</a> is my favorite theme because it looks nice, and the community has built a theme for everything.</p>
<h2 id="home">Home</h2>
<p>I run <a href="https://www.home-assistant.io">Home Assistant</a> at home and have a love/meh relationship with it. I have a liberal usage of <a href="https://www.wyze.com">Wyze cameras and gear</a>, doubling as kid/baby monitors and external perimeter coverage around our gates and cars.</p>
<h2 id="backups">Backups</h2>
<p>Backups should be handled in layers:</p>
<ul>
<li>Quick local backups: Every Mac runs TimeMachine and backups to an encrypted, external drive. I built my NVMe backup drive solution that I will write about sometime.</li>
<li>External, off-sight backups: I store external copies of my data on <a href="https://www.backblaze.com">BackBlaze</a>.</li>
<li>I keep a few big, noisy 8TB external backup drives for occasional snapshots.</li>
</ul>
<h2 id="file-syncing">File Syncing</h2>
<ul>
<li>My documents are backed up to iCloud.</li>
<li>I use <a href="https://syncthing.net">Syncthing</a> to sync my Macs and NUC. I use this to sync projects across machines.</li>
</ul>
<h2 id="development">Development</h2>
<p><a href="https://www.sublimetext.com">SublimeText</a> is my go-to text/code editor. Search is blazingly fast, runs amazingly well on both old and new hardware, and is a stable product.</p>
<p>Almost every Django project I work on runs in a container using <a href="https://docs.docker.com/compose/">Docker Compose</a> or <a href="https://orbstack.dev">Orbstack</a>. Orbstack is nice and fast, but I have many legacy projects in Docker, and the migration only worked so well.</p>
<p>I use the <a href="https://github.com/github/scripts-to-rule-them-all">“scripts to rule them all”</a> pattern because I work on tons of projects, and I never want to think twice about how to start, stop, update, open a console/shell, etc, and it does the job nicely. I will write about this later.</p>
<p>I prefer to script project workflows using <a href="https://github.com/casey/just">Just</a> because it’s a one-file install that doesn’t require a large development stack. It runs well both inside and outside of a container.</p>
<p>I wrote another post about <a href="https://micro.webology.dev/2024/02/10/python-development-on.html">Python Development on macOS Notes: pyenv and pyenv-virtualenvwrapper</a> setup.</p>
<p>I use <a href="https://direnv.net">direnv</a> to manage my environment variables.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Overall, I’m pretty happy with everything. I would like the same Dell monitor setup for the office and a TS4 dock, but everything works.</p>
Office Hours
https://micro.webology.dev/2024/02/17/office-hours/
Sat, 17 Feb 2024 19:51:55 -0600http://webology.micro.blog/2024/02/17/office-hours/<p>Please note: Just so you know, Grammarly told me my writing is fine. So please blame Grammarly and not me for the typos.</p>
<p>Last summer, I started hosting Office Hours on Friday afternoons. The basic idea around hosting office hours was an excuse to work on various projects and invite people to join to work on whatever projects they wanted to end their week.</p>
<p>Friday afternoons work well for me because I will have finished every task I wanted for the week, or anything left over will take me longer than an afternoon to complete, so I might as well start those on Monday instead.</p>
<p>My first session had 3 to 6 people, and then each week started slowly growing, and our last session had ~15 people. While half of the people who join each week I have known and met outside of a computer, more than half of the people who have joined office hours I have never met in person before. We have had people from all over the world, including Africa, the UK, the Netherlands, India, and Alaska, join in to chat while working on projects.</p>
<p>My humble goals for this time have always been to keep things light and serve as a welcoming space where people can join to discuss Python and Django, work on whatever project they want, and ask questions.</p>
<p>My goal wasn’t to build a community, even if it feels like one from seeing familiar faces week after week. So there is no Discord channel, mailing list, or iCal invite.</p>
<p>Instead, I have a private GitHub gist with the invite details, the starting date + time, and a Zoom link. I DM people who ask or people find the link from having accessed it before. I have not optimized my process for growth, but it works.</p>
<p>On weeks when my son has basketball practice, I have the freedom of moving the time around earlier or later, and to my surprise, a friendly group of people always join and trickle in over the time that I have to keep the Zoom meeting up.</p>
<p>On weeks when I’m out of town or need that Friday afternoon to hit a deadline, I update the gist to let people know that we’ll meet again in a few weeks. The success of office hours is not forcing them and letting people know that we have them most weeks but not every week when something is up.</p>
<p>I intentionally created a space where I don’t have the pressure to feel “on,” which is why it works for me, and I hope people will attend, too. There is no agenda, and I do my best to ensure everyone can talk and feel included while hanging out.</p>
<p>My stretch goal for 2024 might be to add a page on my website with the date of our next open hours that I can share with anyone curious. Maybe I’ll even use a nice font for the page, but for now, I think the gist works as well as anything.</p>
The DACO Stack
https://micro.webology.dev/2024/02/15/the-daco-stack/
Thu, 15 Feb 2024 20:36:33 -0600http://webology.micro.blog/2024/02/15/the-daco-stack/<p>For the last few months, I have used the Django + Adam Johnson + Carlton Gibson + Oliver Andrich stack, or what I’m calling the DACO stack. Maybe the DjACO stack rolls off the tongue more easily.</p>
<p>The DACO stack combines new Django tech with wrappers around libraries like Heroicons, TailwindCSS, and htmx, which integrates a nice modern front-end development experience with Django.</p>
<h2 id="carltongibsonneapolitan"><code>carltongibson/neapolitan</code></h2>
<p>The <a href="https://github.com/carltongibson/neapolitan">Neapolitan</a> project brings CRUD views to Django in what feels like a marriage between the Django Admin meets Django Rest Framework’s model viewsets but focused on the front-end. Neapolitan gives you CRUD views for your application in a few lines of Python code.</p>
<p>I have used it on several projects where I wanted to quickly build a front end around some data and give a limited number of people access to help maintain it.</p>
<h2 id="adamchainzdjango-htmx"><code>adamchainz/django-htmx</code></h2>
<p>Adam’s <code>django-htmx</code> adds htmx support to Django.</p>
<p><a href="https://github.com/adamchainz/django-htmx">https://github.com/adamchainz/django-htmx</a></p>
<h2 id="carltongibsondjango-template-partials"><code>carltongibson/django-template-partials</code></h2>
<p>Carlton’s <code>django-template-partials</code> project helps create reusable inline template blocks. When paired with <code>django-htmx</code>, we can render a form, search results, and individual table rows without rewriting the web page.</p>
<p><a href="https://github.com/carltongibson/django-template-partials">https://github.com/carltongibson/django-template-partials</a></p>
<h2 id="adamchainzheroicons"><code>adamchainz/heroicons</code></h2>
<p>Adam brings the <a href="https://heroicons.com">Heroicons</a> library to Django, a series of SVG images that are nice for navigation menus and anywhere you might want to embed an icon image quickly. Heroicons is one of my goto libraries for quickly adding extra polish for apps that I might normally fall back to boring text links.</p>
<p><a href="https://github.com/adamchainz/heroicons">https://github.com/adamchainz/heroicons</a></p>
<h2 id="oliverandrichdjango-tailwind-cli"><code>oliverandrich/django-tailwind-cli</code></h2>
<p>Oliver’s <code>django-tailwind-cli</code> project integrates Tailwind CSS
into Django, includes a live reload server, and enables <code>python manage.py tailwind</code> to work.</p>
<p><a href="https://github.com/oliverandrich/django-tailwind-cli">https://github.com/oliverandrich/django-tailwind-cli</a></p>
<h2 id="conclusion">Conclusion</h2>
<p>Try the DACO stack and let me know if it saves you time and if you find some new tools to add to it.</p>
Year in review 2023
https://micro.webology.dev/2024/02/12/year-in-review/
Mon, 12 Feb 2024 19:39:38 -0600http://webology.micro.blog/2024/02/12/year-in-review/<p>I write one of these posts every year, and I manage never to publish them. Since I’m trying to write one post a day for all of February, tonight I was revisiting some of my drafts, and this felt like a very Monday post to dust off and publish.</p>
<p>If 2023 had a theme, it was stepping down and stepping back.</p>
<h2 id="family">Family</h2>
<ul>
<li>๐ We took our first family trip with most of my immediate family for the first time in too many years in Surfside Beach, Texas</li>
<li>๐ I bought a 2023 Subaru Ascent to replace my 2001 Nissan Pathfinder</li>
<li>6๏ธโฃ My oldest turned six and started Kindergarten</li>
<li>2๏ธโฃ My youngest turned two and started preschool</li>
</ul>
<h2 id="djangopython">Django/Python</h2>
<ul>
<li>โ <a href="https://www.python.org/psf/board/#id3">PSF Board</a>: I served two terms over five years and did not run for a third term. During my tenure, I served as Treasurer and then Vice Chair. I’m still on the Code of Conduct WG but left the Fiscal and PyCon US Committees.</li>
<li>โ <a href="https://www.defna.org/about/">DEFNA</a>: I stepped down as president after eight years on the board. I technically stepped down two or three years ago, but I filled in for a year to help out while we transitioned back to running an in-person conference. I’m still on the board and helping with DjangoCon US, but I’m doing so with fewer balls in the air.</li>
<li>๐ <a href="https://2023.djangocon.us">DjangoCon US 2023</a> We held a successful conference in Durham, North Carolina.</li>
<li>๐ Launched <a href="https://jobs.django-news.com">Django News Jobs</a></li>
</ul>
<h2 id="community">Community</h2>
<ul>
<li>๐๏ธ Attended/co-hosted dozens of <a href="https://www.conferencechats.org">Conference Chats</a> meetups</li>
<li>๐ I started weekly(ish) office hours on Fridays as an excuse to work on projects and hang out with community members.</li>
<li>๐๏ธ Attended <a href="https://us.pycon.org/2023/">PyCon US</a> virtually</li>
<li>๐๏ธ Attended <a href="https://2023.pygotham.tv">PyGotham</a> virtually</li>
<li>๐๏ธ Attended <a href="https://2023.djangocon.us">DjangoCon US</a></li>
<li>โ๏ธ I joined a weekly writing group</li>
<li>๐ซ I joined a bunch of interesting Discord communities</li>
</ul>
Python Development on macOS Notes: pyenv and pyenv-virtualenvwrapper
https://micro.webology.dev/2024/02/10/python-development-on-macos-notes/
Sat, 10 Feb 2024 20:04:43 -0600http://webology.micro.blog/2024/02/10/python-development-on-macos-notes/<p>Here are my notes for using <a href="https://github.com/pyenv/pyenv"><code>pyenv</code></a> and <a href="https://github.com/pyenv/pyenv-virtualenvwrapper"><code>pyenv-virtualenvwrapper</code></a> on macOS.</p>
<p>I use <code>pyenv</code> to install and manage multiple Python versions on the same machine. <code>pyenv</code> makes it possible to upgrade my default version of Python without breaking every project that was created with the older version until I’m ready to upgrade them.</p>
<p>I use <code>pyenv-virtualenvwrapper</code> to manage my projects. <code>pyenv-virtualenvwrapper</code> is a set of tools that make it easier to create, delete, copy, and manage Python virtual environments.</p>
<h2 id="install-pyenv-through-homebrew">Install pyenv through Homebrew</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">brew update
brew install pyenv
brew install pyenv-virtualenvwrapper
</code></pre></div><h2 id="configuring-pyenv-to-work-with-my-shell">Configuring pyenv to work with my shell</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#75715e">## set a variable for finding pyenv</span>
export PYENV_ROOT<span style="color:#f92672">=</span><span style="color:#e6db74">"</span><span style="color:#e6db74">${</span>HOME<span style="color:#e6db74">}</span><span style="color:#e6db74">/.pyenv"</span>
<span style="color:#75715e"># 2024-02 Python pyenv settings</span>
<span style="color:#66d9ef">if</span> command -v pyenv > /dev/null; <span style="color:#66d9ef">then</span>
eval <span style="color:#e6db74">"</span><span style="color:#66d9ef">$(</span>pyenv init --path<span style="color:#66d9ef">)</span><span style="color:#e6db74">"</span>;
eval <span style="color:#e6db74">"</span><span style="color:#66d9ef">$(</span>pyenv init -<span style="color:#66d9ef">)</span><span style="color:#e6db74">"</span>;
pyenv virtualenvwrapper_lazy
<span style="color:#66d9ef">fi</span>
</code></pre></div><h2 id="install-the-latest-python-versions-using-pyenv">Install the “latest” Python versions using pyenv</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell"><span style="color:#75715e"># install latest Python 3.11</span>
pyenv install 3.11:latest
<span style="color:#75715e"># to see which version of 3.11 was installed</span>
pyenv latest 3.11
> 3.11.7
<span style="color:#75715e"># install latest Python 3.12</span>
pyenv install 3.12:latest
<span style="color:#75715e"># to see which version of 3.12 was installed</span>
pyenv latest 3.12
> 3.12.1
<span style="color:#75715e"># set each version globally to our latest python 3.x version</span>
pyenv global 3.11.7 3.12.1
</code></pre></div><h2 id="upgrading-python-versions">Upgrading Python versions</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">pyenv install --skip-existing 3.11:latest
pyenv install --skip-existing 3.12:latest
</code></pre></div><h2 id="list-every-version-of-python-that-is-installable">List every version of Python that is installable</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">pyenv install --list
</code></pre></div><h2 id="list-every-installed-version-of-python">List every installed version of Python</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">pyenv versions
</code></pre></div>The Power of Semi-Automation: Why Less Can Be More
https://micro.webology.dev/2024/02/07/the-power-of-semiautomation-why/
Wed, 07 Feb 2024 23:21:58 -0600http://webology.micro.blog/2024/02/07/the-power-of-semiautomation-why/<p>According to the <a href="https://en.wikipedia.org/wiki/Ninety%E2%80%93ninety_rule">ninety-ninety rule</a>, the final 10 percent of code needed to automate a task completely can take up 90 percent of the development time. This disproportionate effort is why I recommend that everyone consider semi-automating tasks and skipping the last 10%.</p>
<h2 id="quick-wins">Quick wins</h2>
<p>My goal with semi-automating tasks is to reduce the scope of a project to make automating the task easier and quicker.
The more mundane and repetitive the task, the more likely I will prioritize automating it.</p>
<p>My favorite tasks to automate fall are quick wins or redundant processes.
We use Toggl at work to log our billable hours and time spent on various projects.
We have daily stand-ups in Slack, so everyone can keep up with everyone and be on the same page about vacations or might need another set of eyes on a project.</p>
<p>Using the Toggl API, I wrote a Python script that exports my notes so I can copy and paste them into Slack.
I wrote a Python stand-up script that I run daily to see my notes from the previous day or week.</p>
<p>I could have over-engineered my script to attempt to post to Slack for me, but it turns out that all I needed was a better way to copy and paste from Toggl into Slack.</p>
<h2 id="why-do-i-semi-automate-tasks">Why do I semi-automate tasks?</h2>
<p><strong>Semi-automation is easier.</strong> The benefit of semi-automating tasks is to reduce the scope to be a quick, productive win.</p>
<p><strong>Semi-automation saves time.</strong> I often spend 30 minutes writing a Python script, saving me 15 minutes or more daily.</p>
<p><strong>Semi-automation saves wasted effort.</strong> For my daily stand-up project, I can use an existing Python library to access the Toggl API and display all my entries for a day or range of dates.
Most of the time, I will copy these notes as they are and paste them into Slack, or I will summarize them.</p>
<p><strong>Semi-automation takes less maintenance.</strong> Since my Python script is wrapping an existing API, there is little to no maintenance.
If the author decides they don’t want to maintain a Toggl library anymore, switching to another library is less effort and much less complex than if I were trying to maintain my library.</p>
<p><strong>Semi-automation is often good enough.</strong> I thought about writing a Slackbot to post updates for me, but I quickly learned that it would be a bigger hassle and ultimately not worth the effort.
My most significant pain point was having to pull up Toggl to get information out of it, and with my stand-up script, I could focus on writing better summaries while logging my hours instead.</p>
<h2 id="types-of-projects-i-like-to-semi-automate">Types of projects I like to semi-automate</h2>
<p>The projects I like to semi-automate are repetitive tasks I have to do daily, weekly, and frequent enough to warrant
The more repetitive a task is, the more likely I will write a Python script or <a href="https://github.com/casey/just">casey/just</a> justfile recipe to automate it.</p>
<ul>
<li>“copy to clipboard” page</li>
<li>my daily stand-ups and monthly reports, which I pull from Toggl</li>
<li>use RSS feeds to help automate the collection of articles to review for a weekly newsletter</li>
<li>use justfile recipes for every project and even system-level macOS updates, so I don’t have to remember how to update or run my projects</li>
</ul>
<h2 id="migrating-unfinished-projects">Migrating unfinished projects</h2>
<p>Once I realized that some of my unfinished projects were too ambitious, I started revisiting them to reduce the scope to something I could semi-automate to complete them.</p>
Django Queryset to Markdown Table
https://micro.webology.dev/2024/02/06/django-queryset-to-markdown-table/
Tue, 06 Feb 2024 21:24:25 -0600http://webology.micro.blog/2024/02/06/django-queryset-to-markdown-table/<p>Today, I asked on Mastodon if anyone knew of a good solution for generating a Markdown table from a Django queryset. Typically, people want to convert markdown into HTML, but this was a case where I needed to export data from Django to a static website.</p>
<p>Not missing a beat, <a href="https://mastodon.social/@[email protected]/111886900012564229">Katie McLaughlin</a> recommended the <a href="https://github.com/astanin/python-tabulate">python-tabulate</a> library, which is what I was looking for.</p>
<p>Since I was short on time tonight, I asked ChatGPT to:</p>
<ol>
<li>Write a queryset to markdown function using the Python tabulate library.</li>
<li>Add Python types support (because it’s 2024)</li>
<li>Rewrite the docstring</li>
</ol>
<p>Once the code looked right, I loaded up my local copy of <a href="https://jobs.django-news.com">Django News Jobs</a>, and I created a quick Django management command.</p>
<p><a href="https://github.com/GaretJax/django-click">django-click</a> is my go-to library for quickly writing CLI apps or what Django calls a management command. django-click reduces the boilerplate needed to write a management command down to one import, one decorator, and one function, and you get the best of the Python <a href="https://click.palletsprojects.com/en/8.1.x/">click</a> library with everything Django has to offer.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># management/commands/table.py</span>
<span style="color:#f92672">import</span> djclick <span style="color:#66d9ef">as</span> click
<span style="color:#f92672">from</span> django.db.models.query <span style="color:#f92672">import</span> QuerySet
<span style="color:#f92672">from</span> tabulate <span style="color:#f92672">import</span> tabulate
<span style="color:#f92672">from</span> jobs.models <span style="color:#f92672">import</span> JobListing
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">queryset_to_markdown</span>(queryset: QuerySet, fields: list[str] <span style="color:#f92672">|</span> <span style="color:#66d9ef">None</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>) <span style="color:#f92672">-></span> str:
<span style="color:#e6db74">"""
</span><span style="color:#e6db74"> Convert a Django queryset to a markdown table using the tabulate library.
</span><span style="color:#e6db74">
</span><span style="color:#e6db74"> Args:
</span><span style="color:#e6db74"> queryset (QuerySet): The Django queryset to convert into a markdown table.
</span><span style="color:#e6db74"> fields (list[str] | None, optional): A list of strings specifying the model fields to include in the table.
</span><span style="color:#e6db74"> If None, all fields from the model will be included. Defaults to None.
</span><span style="color:#e6db74">
</span><span style="color:#e6db74"> Returns:
</span><span style="color:#e6db74"> str: A string representing the markdown table formatted according to GitHub-flavored Markdown.
</span><span style="color:#e6db74"> """</span>
<span style="color:#75715e"># If fields are not specified, use all fields from the model</span>
<span style="color:#66d9ef">if</span> fields <span style="color:#f92672">is</span> <span style="color:#66d9ef">None</span>:
fields <span style="color:#f92672">=</span> [field<span style="color:#f92672">.</span>name <span style="color:#66d9ef">for</span> field <span style="color:#f92672">in</span> queryset<span style="color:#f92672">.</span>model<span style="color:#f92672">.</span>_meta<span style="color:#f92672">.</span>fields]
<span style="color:#75715e"># Prepare data for tabulation</span>
data <span style="color:#f92672">=</span> []
<span style="color:#66d9ef">for</span> obj <span style="color:#f92672">in</span> queryset:
row <span style="color:#f92672">=</span> [getattr(obj, field) <span style="color:#66d9ef">for</span> field <span style="color:#f92672">in</span> fields]
data<span style="color:#f92672">.</span>append(row)
<span style="color:#75715e"># Generate markdown table</span>
markdown_table <span style="color:#f92672">=</span> tabulate(data, headers<span style="color:#f92672">=</span>fields, tablefmt<span style="color:#f92672">=</span><span style="color:#e6db74">"github"</span>)
<span style="color:#66d9ef">return</span> markdown_table
<span style="color:#a6e22e">@click</span><span style="color:#f92672">.</span>command()
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">command</span>():
job_listings <span style="color:#f92672">=</span> JobListing<span style="color:#f92672">.</span>objects<span style="color:#f92672">.</span>all()<span style="color:#f92672">.</span>active()<span style="color:#f92672">.</span>order_by(<span style="color:#e6db74">"-published"</span>)
print(queryset_to_markdown(job_listings, fields<span style="color:#f92672">=</span>[<span style="color:#e6db74">"title"</span>, <span style="color:#e6db74">"employer_name"</span>]))
</code></pre></div><p>Running <code>python manage.py table</code> gave me the following table. Please note: I removed the <code>employer_name</code> column to fit the output in my article.</p>
<pre>
| title |
|-----------------------------------------------------|
| Software Engineer (Terraso) |
| Django Fellow |
| Senior Django Developer for Boutique Digital Agency |
| Software Engineer |
| Business Systems Analyst |
| Python Django E-Commerce Full Stack |
| Principal Software Engineer |
| Michigan Online, Software Engineer |
| Full Stack Python Developer |
| Senior Backend Developer - Supportsite Team |
| Python Backend Developer |
| Python / Django Developer (f/m/d) |
| Full Stack Python Developer |
| Software Engineer - Ubuntu Systems Management |
| Senior Python/Django Engineer |
</pre>
<p>Overall, I’m happy with this approach. Django is an excellent framework for quickly prototyping and solving these problems.</p>
<p>I decided to write a management command instead of a view because markdown tables are very readable from the command line. I plan to convert it to a Django view to automate embedding tables like this into other projects.</p>
How to test with Django and pytest fixtures
https://micro.webology.dev/2024/02/05/how-to-test-with-django/
Mon, 05 Feb 2024 22:57:33 -0600http://webology.micro.blog/2024/02/05/how-to-test-with-django/<p>Here are some notes on how I like to write tests for a Django app that tests a model down to a view.</p>
<h2 id="django-model--pytest-fixture">Django Model + pytest Fixture</h2>
<p>This part is a mouthful, but I prefer to use the <a href="https://github.com/model-bakers/model_bakery">Model Bakery</a> library to automatically create instances of Django models (aka a fixture) with good test data instead of manually creating them. This approach saves time and adds some variety to my test data.</p>
<p>We will also use the pytest fixture function to create and return an instance of a Django model using our Model Bakery fixture. This pytest fixture can be used in multiple test cases, ensuring consistency and reducing boilerplate code.</p>
<p>Every app in my project contains a <code>fixtures.py</code> file. My news app has a <code>Category</code> and a <code>Post</code> model, and my fixture looks like this.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># news/tests/fixtures.py</span>
<span style="color:#f92672">import</span> pytest
<span style="color:#f92672">from</span> model_bakery <span style="color:#f92672">import</span> baker
<span style="color:#a6e22e">@pytest</span><span style="color:#f92672">.</span>fixture()
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">category</span>(db):
<span style="color:#66d9ef">return</span> baker<span style="color:#f92672">.</span>make(<span style="color:#e6db74">"news.Category"</span>, name<span style="color:#f92672">=</span><span style="color:#e6db74">"Category Name"</span>)
<span style="color:#a6e22e">@pytest</span><span style="color:#f92672">.</span>fixture()
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">post</span>(db, category):
<span style="color:#66d9ef">return</span> baker<span style="color:#f92672">.</span>make(<span style="color:#e6db74">"news.Post"</span>, title<span style="color:#f92672">=</span><span style="color:#e6db74">"Post Title"</span>, category<span style="color:#f92672">=</span>category)
</code></pre></div><p>Please note that a <code>post</code> fixture can accept <code>category</code> fixture as an argument.</p>
<h2 id="configuration">Configuration</h2>
<p>At the top of my project, we will have a <code>conftest.py</code> file. This is where we tell pytest to look for our fixtures so they may be automatically loaded.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># conftest.py</span>
pytest_plugins <span style="color:#f92672">=</span> [<span style="color:#e6db74">"news.tests.fixtures"</span>]
</code></pre></div><h2 id="testing-our-models">Testing our models</h2>
<p>Next, we write a very basic test to ensure our fixture can create an instance of a model.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># news/tests/test_models.py</span>
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">test_get_category</span>(category):
<span style="color:#66d9ef">assert</span> category<span style="color:#f92672">.</span>name <span style="color:#f92672">==</span> <span style="color:#e6db74">"Category Name"</span>
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">test_get_post</span>(post):
<span style="color:#66d9ef">assert</span> post<span style="color:#f92672">.</span>title <span style="color:#f92672">==</span> <span style="color:#e6db74">"Post Title"</span>
</code></pre></div><h2 id="testing-our-views">Testing our views</h2>
<p>I prefer working with <a href="https://github.com/revsys/django-test-plus"><code>django-test-plus</code></a> because it helps make writing tests cleaner and more straightforward. Then, for every view we have, we will write a test to ensure that our URL patterns match our expected routes and that we return a predicted status code.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-python" data-lang="python"><span style="color:#75715e"># news/tests/test_views.py</span>
<span style="color:#f92672">import</span> pytest
<span style="color:#75715e"># test that our view resolves to the right uri path</span>
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">test_post_uri</span>(tp):
expected_url <span style="color:#f92672">=</span> <span style="color:#e6db74">"/"</span>
reversed_url <span style="color:#f92672">=</span> tp<span style="color:#f92672">.</span>reverse(<span style="color:#e6db74">"post-list"</span>)
<span style="color:#66d9ef">assert</span> expected_url <span style="color:#f92672">==</span> reversed_url
<span style="color:#75715e"># test that our view works</span>
<span style="color:#66d9ef">def</span> <span style="color:#a6e22e">test_post_get</span>(db, tp):
response <span style="color:#f92672">=</span> tp<span style="color:#f92672">.</span>get(<span style="color:#e6db74">"post-list"</span>)
tp<span style="color:#f92672">.</span>assert_http_200_ok(response)
</code></pre></div><h2 id="running-tests">Running tests</h2>
<p>To run our tests, we can run pytest.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4"><code class="language-shell" data-lang="shell">pytest
</code></pre></div>The Django apps I actually use (rough cut)
https://micro.webology.dev/2024/02/04/the-django-apps-i-actually/
Sun, 04 Feb 2024 23:20:30 -0600http://webology.micro.blog/2024/02/04/the-django-apps-i-actually/<p>This is an updated version of my response to the <a href="https://forum.djangoproject.com/t/top-5-3rd-party-packages/391">Top 5 3rd party packages</a> from the Django Forum.</p>
<p>I plan on writing something more in-depth, but life, family, and getting the kids ready for the week got away from me today. Here is my list in case anyone finds them to be helpful.</p>
<ul>
<li><code>django-allauth</code></li>
<li><code>django-click</code></li>
<li><code>django-htmx</code></li>
<li><code>django-q2</code></li>
<li><code>django-test-plus</code></li>
<li><code>django-tailwind-cli</code></li>
<li><code>django-template-partials</code></li>
<li><code>environs[django]</code></li>
<li><code>heroicons[django]</code></li>
<li><code>neapolitan</code></li>
<li><code>python-slugify</code></li>
<li><code>ruff</code> via <code>pre-commit</code> but previously was <code>black</code></li>
<li><code>whitenoise</code></li>
</ul>
<h2 id="projects-i-no-longer-use">Projects I no longer use</h2>
<ul>
<li><code>django-dbbackup</code> I dropped this app for <code>DSLR</code> and then I more recently dropped I dropped them both because I didn’t want to install the extra Postgres files in my containers. I use <code>pg_backup</code>, <code>pg_restore</code>, and <code>psql</code> via the official</li>
</ul>
Choosing the Right Python and Django Versions for Your Projects
https://micro.webology.dev/2024/02/01/choosing-the-right-python-and/
Thu, 01 Feb 2024 23:32:08 -0600http://webology.micro.blog/2024/02/01/choosing-the-right-python-and/<p>When deciding when to adopt a new <em>major</em> or <em>minor</em> Python or Django version, I prefer to wait until the 3rd or 4th patch release because reliability significantly improves.</p>
<p>Python and Django <code>{major}.{minor}.{patch>2}</code> releases are always more stable than <code>{patch<3}</code> releases.</p>
<h3 id="understanding-version-numbers">Understanding Version Numbers</h3>
<p>Version numbers typically follow a <code>{major}.{minor}.{patch}</code> format. For instance, in Django 5.0.2, “5” is the major version, “0” the minor, and “2” the patch. I’ve found that a version reaching its <code>.2</code> or <code>.3</code> patch release (like Django 5.0.2 or 5.0.3) is generally a reliable indicator of stability.</p>
<h3 id="why-wait-for-the-3rd-or-4th-patch-release">Why Wait for the 3rd or 4th Patch Release?</h3>
<p>The early releases of any major software version can be unpredictable. Despite Python and Django’s commendable track record for quality, the broader ecosystem of packages and tools that integrate with them often requires time to catch up. This adjustment period is critical for ensuring compatibility and stability, reducing the risk of unexpected issues in production environments.</p>
<h3 id="my-experience">My Experience</h3>
<p>Through trial and error, I’ve observed that holding off until at least the 2nd patch release mitigates around 90% of potential issues. However, waiting for the 3rd or 4th patch guarantees that most significant bugs have been fixed.</p>
<h3 id="recommendations-for-implementation">Recommendations for Implementation:</h3>
<ul>
<li>
<p><strong>Stay Informed</strong>: Regularly review the release notes for Python and Django. This will help you understand the scope of changes and the introduction of any new features or critical fixes.</p>
</li>
<li>
<p><strong>Use a Testing Matrix</strong>: If your project employs Continuous Integration (CI), include the latest versions of Python and Django in your testing matrix sooner rather than later. This proactive measure can help identify compatibility issues early, saving you time and reducing the risk of deploying unstable code to production.</p>
</li>
<li>
<p><strong>Community Feedback</strong>: Pay attention to feedback from the Python and Django communities. I keep a good feel of the pulse by checking social media to see what developers are having issues with. I also like to check open and recently closed GitHub Issues and Pull Requests for packages I use to see what was broken by a new release and how to fix it.</p>
</li>
</ul>