<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Python | Bas Nijholt</title><link>https://www.nijho.lt/tag/python/</link><atom:link href="https://www.nijho.lt/tag/python/index.xml" rel="self" type="application/rss+xml"/><description>Python</description><generator>Hugo</generator><language>en-us</language><copyright>© 2026</copyright><lastBuildDate>Sat, 26 Sep 2026 00:00:00 +0000</lastBuildDate><image><url>https://www.nijho.lt/media/icon_hu_408a8f6cef43e327.png</url><title>Python</title><link>https://www.nijho.lt/tag/python/</link></image><item><title>zfs-tenant</title><link>https://www.nijho.lt/project/zfs-tenant/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/zfs-tenant/</guid><description/></item><item><title>pytest-shm</title><link>https://www.nijho.lt/project/pytest-shm/</link><pubDate>Fri, 25 Sep 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/pytest-shm/</guid><description/></item><item><title>ZFS Unlock</title><link>https://www.nijho.lt/project/zfs-unlock/</link><pubDate>Mon, 29 Jun 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/zfs-unlock/</guid><description/></item><item><title>MindRoom: AI agents that live in Matrix and work everywhere 🧠</title><link>https://www.nijho.lt/post/mindroom/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/mindroom/</guid><description>&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-the-problem-your-ai-is-trapped-in-apps"&gt;1. The problem: your AI is trapped in apps&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-what-is-mindroom"&gt;2. What is MindRoom?&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-how-it-actually-works"&gt;3. How it actually works&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-building-on-matrix-the-good-and-the-tricky"&gt;4. Building on Matrix: the good and the tricky&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-memory-that-follows-you"&gt;5. Memory that follows you&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-100-built-in-tool-integrations"&gt;6. 100+ built-in tool integrations&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#7-teams-agents-that-collaborate"&gt;7. Teams: agents that collaborate&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#8-hot-reload-change-config-and-plugins-without-downtime"&gt;8. Hot-reload: change config and plugins without downtime&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#9-voice-scheduling-and-other-features"&gt;9. Voice, scheduling, and other features&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#10-the-obsession-the-saas-dream-and-the-burnout"&gt;10. The obsession, the SaaS dream, and the burnout&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#11-openclaw-and-why-it-matters"&gt;11. OpenClaw and why it matters&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#12-whats-next"&gt;12. What&amp;rsquo;s next&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#links-and-resources"&gt;Links and resources&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-the-problem-your-ai-is-trapped-in-apps"&gt;1. The problem: your AI is trapped in apps &lt;a class="anchor" href="#1-the-problem-your-ai-is-trapped-in-apps" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Programming is my biggest passion in life.&#10;I&amp;rsquo;ve been actively involved in open source for over ten years now, maintaining &lt;a href="https://github.com/basnijholt" target="_blank" rel="noopener"&gt;40+ installable packages&lt;/a&gt; across Python, JavaScript, and Rust.&#10;Some of these have over 100,000 users; others have exactly one—me.&#10;I mention this because it matters for what comes next: when AI coding tools arrived, I didn&amp;rsquo;t start from zero.&#10;As I wrote about in my &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;agentic coding post&lt;/a&gt;, AI has completely transformed how I work—when I&amp;rsquo;m AFK, I regularly build full features or entire projects by &lt;a href="https://www.nijho.lt/post/agentic-mobile-workflow/"&gt;dictating to my phone&lt;/a&gt; and routing it through my own &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;transcription software&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Through all of this, I became deeply convinced that AI agents are the future.&#10;Not just a useful tool for programmers—a fundamental shift in how &lt;em&gt;everyone&lt;/em&gt; will interact with computers.&lt;/p&gt;&#10;&lt;p&gt;But if that&amp;rsquo;s true, then the way agents work today is broken.&lt;/p&gt;&#10;&lt;p&gt;We&amp;rsquo;ve built artificial barriers between artificial intelligences.&#10;ChatGPT knows you in one tab, Claude knows you in another, your Slack bot knows you in a third.&#10;Your email AI can&amp;rsquo;t tell your calendar AI about that meeting invitation.&#10;Your coding assistant has no idea about the project specs sitting in your document AI.&#10;Switch platforms, and your AI starts from scratch.&lt;/p&gt;&#10;&lt;p&gt;Most agent frameworks require you to &lt;em&gt;program&lt;/em&gt; them.&#10;That works for developers like me, but if agents are truly the future, they need to work for everyone.&#10;Non-programmers don&amp;rsquo;t want to write code—they just want a chat.&lt;/p&gt;&#10;&lt;p&gt;And then there&amp;rsquo;s privacy.&#10;I&amp;rsquo;m fine sharing my email with Gemini—Google already owns my Gmail.&#10;But do I want to send my financial data, my health records, or my personal notes to a cloud provider?&#10;Not really.&#10;I&amp;rsquo;d rather use a local model for sensitive tasks and pick the best, cheapest Chinese model for general-purpose deep research.&lt;/p&gt;&#10;&lt;p&gt;So I built &lt;strong&gt;MindRoom&lt;/strong&gt; on a chat protocol—if people just want a chat, give them a chat.&#10;Agents live in the same rooms as you, collaborate in threads, and follow you across every platform.&#10;The first commit landed on July 29, 2025 (four months before OpenClaw&amp;rsquo;s first commit 😅), so by the time I&amp;rsquo;m publishing this MindRoom has been with me for nearly eleven months: first as an obsessive side project, then, since March, with much more sustained focus.&#10;At first I got so obsessed with it that I eventually burned out and had to step away completely.&lt;/p&gt;&#10;&lt;p&gt;Recently, seeing &lt;a href="https://openclaw.ai" target="_blank" rel="noopener"&gt;OpenClaw&lt;/a&gt; gain massive traction solving a similar problem reminded me that what I&amp;rsquo;d been building wasn&amp;rsquo;t just a niche obsession.&#10;So here I am, dusting off MindRoom and writing about what it is, how it works, and why I think the approach still matters.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; MindRoom is an open-source system that creates AI agents living inside the &lt;a href="https://matrix.org/" target="_blank" rel="noopener"&gt;Matrix protocol&lt;/a&gt;.&#10;Because Matrix has bridges to Slack, Telegram, Discord, WhatsApp, and everything else, your agents can follow you across every platform—with persistent memory, 100+ built-in tool integrations, and multi-agent collaboration.&#10;Think of it as &amp;ldquo;your AI assistant, but it lives in Matrix and works everywhere.&amp;rdquo;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;figure id="figure-live-chatmindroomchat-personal-thread-with-the-prompt-explain-how-mindroom-and-what-you-can-do-mind-answers-in-the-real-matrix-room-using-tool-calls-and-the-codex-backed-local-mindroom-runtime"&gt;&lt;img src="https://www.nijho.lt/post/mindroom/mindroom-in-action_hu_5c1001a61c1ce0a1.webp" srcset="https://www.nijho.lt/post/mindroom/mindroom-in-action_hu_cba228417596d894.webp 400w, https://www.nijho.lt/post/mindroom/mindroom-in-action_hu_5c1001a61c1ce0a1.webp 760w, https://www.nijho.lt/post/mindroom/mindroom-in-action_hu_46631ca52a25e910.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="489" alt="Live MindRoom Personal thread showing the prompt Explain how MindRoom and what you can do, followed by Mind explaining Matrix agents, tools, memory, and skills." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Live chat.mindroom.chat Personal thread with the prompt: “Explain how MindRoom and what you can do.” Mind answers in the real Matrix room, using tool calls and the Codex-backed local MindRoom runtime.&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="2-what-is-mindroom"&gt;2. What is MindRoom? &lt;a class="anchor" href="#2-what-is-mindroom" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;MindRoom is an open-source system that creates AI agents living inside the &lt;a href="https://matrix.org/" target="_blank" rel="noopener"&gt;Matrix protocol&lt;/a&gt;.&#10;If you&amp;rsquo;re not familiar with Matrix—it&amp;rsquo;s a federated, end-to-end encrypted communication standard.&#10;The same protocol used by the French government for 5.5 million civil servants, by German healthcare for 150K+ organizations, and by the Element app that millions of people use daily.&lt;/p&gt;&#10;&lt;p&gt;The key insight: Matrix has &lt;a href="https://matrix.org/ecosystem/bridges/" target="_blank" rel="noopener"&gt;bridges&lt;/a&gt; to many major platforms—Slack, Telegram, Discord, WhatsApp, IRC, email, even SMS.&lt;/p&gt;&#10;&lt;p&gt;So if your AI agent lives in Matrix, it can reach you on any platform.&#10;One agent, every platform, continuous memory.&lt;/p&gt;&#10;&lt;p&gt;One practical caveat: Matrix bridges vary in maturity. Some, like the &lt;a href="https://github.com/mautrix/telegram" target="_blank" rel="noopener"&gt;Telegram bridge&lt;/a&gt;, work very well, while others can be finicky. Your mileage may vary depending on which platforms you need.&lt;/p&gt;&#10;&lt;p&gt;Here&amp;rsquo;s what a typical setup looks like in &lt;code&gt;config.yaml&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;CodeAgent&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Generate code, manage files, execute shell commands&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;opus-4.8&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;file, shell, github]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;Always read files before modifying them.&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rooms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;lobby, dev]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;research&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ResearchAgent&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Search the web, summarize papers, find information&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;gpt-5.5&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;tavily, arxiv, wikipedia]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rooms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;lobby, research]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;super_team&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Super Team&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;code, research]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;collaborate&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Define your agents, give them tools and rooms, and they show up in Matrix as real users—with avatars, typing indicators, and online status.&lt;/p&gt;&#10;&lt;figure id="figure-mindroom-agents-are-real-matrix-users-with-their-own-accounts-generated-avatars-display-names-room-memberships-and-online-presence"&gt;&lt;img src="https://www.nijho.lt/post/mindroom/mindroom-members_hu_5090cd21f23bad91.webp" srcset="https://www.nijho.lt/post/mindroom/mindroom-members_hu_fbc884b2330f0fe3.webp 400w, https://www.nijho.lt/post/mindroom/mindroom-members_hu_5090cd21f23bad91.webp 760w, https://www.nijho.lt/post/mindroom/mindroom-members_hu_e6a566cfcdf2d915.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="489" alt="Live MindRoom room showing RouterAgent, Mind, and Bas as Matrix users with generated avatars." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;MindRoom agents are real Matrix users with their own accounts, generated avatars, display names, room memberships, and online presence.&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="3-how-it-actually-works"&gt;3. How it actually works &lt;a class="anchor" href="#3-how-it-actually-works" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;At the core sits what I call the &lt;strong&gt;MultiAgentOrchestrator&lt;/strong&gt;—a class in &lt;code&gt;bot.py&lt;/code&gt; that boots every configured entity (router, agents, teams), provisions Matrix user accounts for each one via &lt;a href="https://github.com/matrix-nio/matrix-nio" target="_blank" rel="noopener"&gt;matrix-nio&lt;/a&gt;, and keeps sync loops alive.&#10;The agents themselves are powered by the &lt;a href="https://github.com/agno-agi/agno" target="_blank" rel="noopener"&gt;Agno&lt;/a&gt; framework, which provides a unified interface across AI model providers.&lt;/p&gt;&#10;&lt;p&gt;When someone sends a message in a Matrix room:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Explicit mentions&lt;/strong&gt; go directly to the named agent (&lt;code&gt;@mindroom_code help me debug this&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Thread continuations&lt;/strong&gt; keep the same agent responding (no weird context switches mid-conversation)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;New conversations&lt;/strong&gt; hit the &lt;strong&gt;router&lt;/strong&gt;—an AI that analyzes the message and picks the best agent based on capabilities&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;All conversations happen in threads, which keeps rooms organized.&#10;Agents stream their responses in real-time, editing a single message as they think rather than spamming new ones.&#10;You see tool calls happening live:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;🔧 Tool Call: search_web(query=&amp;#34;matrix protocol bridges&amp;#34;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[waiting for result...]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;✅ search_web result:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;[results here]&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The recording below starts in the main Personal room, sends the prompt &lt;code&gt;Explain how MindRoom and what you can do.&lt;/code&gt;, and then follows MindRoom as it opens the new thread and streams the reply to completion. It is sped up 2.5x to keep the flow skimmable.&lt;/p&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/reduced-motion.3dfea0061c17c60a9a200c61ef1e3b3e98d5bdc4eaa4e0a64b3ed2a4d13c72fd.js" integrity="sha256-Pf6gBhwXxgqaIAxh7x47PpjVvcTqpOCmSz7SpNE8cv0=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&lt;video controls autoplay muted playsinline loop preload="metadata"&gt;&#10;&lt;source src="https://www.nijho.lt/post/mindroom/mindroom-agent-response.mp4" type="video/mp4"&gt;&#10;&lt;/video&gt;&#10;&#10;&lt;h2 id="4-building-on-matrix-the-good-and-the-tricky"&gt;4. Building on Matrix: the good and the tricky &lt;a class="anchor" href="#4-building-on-matrix-the-good-and-the-tricky" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;One of the best things about building on Matrix is what you get for free.&#10;End-to-end encryption, a deeply interoperable chat protocol with bridges to many platforms, a choice of clients, federation between organizations—all of that comes with the protocol.&#10;You don&amp;rsquo;t have to build any of it yourself.&lt;/p&gt;&#10;&lt;p&gt;But because Matrix has such a tight specification, it also brings challenges.&lt;/p&gt;&#10;&lt;p&gt;The protocol doesn&amp;rsquo;t support streaming.&#10;AI agents that think for 30 seconds before dumping a wall of text make for a terrible chat experience, so I hacked streaming in by rapidly editing the same message as new tokens arrive.&#10;An &lt;code&gt;⋯&lt;/code&gt; marker shows while the agent is still thinking—a small touch, but it makes the experience feel responsive and alive.&lt;/p&gt;&#10;&lt;p&gt;There&amp;rsquo;s also a size limit on message content.&#10;That&amp;rsquo;s fine for human chat, but AI responses can get long—especially when tool calls and their results are included.&#10;I worked around this by using Matrix&amp;rsquo;s attachment feature: when a response exceeds the limit, the content continues in an attachment that gets updated as the message keeps streaming in.&#10;This required forking the &lt;a href="https://cinny.in/" target="_blank" rel="noopener"&gt;Cinny&lt;/a&gt; chat client so that attachments display inline rather than as downloadable files, making the whole thing seamless.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;These workarounds are the kind of thing you don&amp;rsquo;t anticipate when you pick a protocol.&#10;Matrix gives you an incredible foundation, but making it work well for AI required bending it in ways it wasn&amp;rsquo;t designed for.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="5-memory-that-follows-you"&gt;5. Memory that follows you &lt;a class="anchor" href="#5-memory-that-follows-you" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;MindRoom currently supports two memory implementations.&lt;/p&gt;&#10;&lt;p&gt;The first is the more traditional semantic memory system built on &lt;a href="https://mem0.ai/" target="_blank" rel="noopener"&gt;Mem0&lt;/a&gt;&amp;rsquo;s &lt;code&gt;AsyncMemory&lt;/code&gt;, with configurable embedding providers (&lt;a href="https://platform.openai.com/docs/guides/embeddings" target="_blank" rel="noopener"&gt;OpenAI&lt;/a&gt;, &lt;a href="https://ollama.com/" target="_blank" rel="noopener"&gt;Ollama&lt;/a&gt;, or HuggingFace) and vector storage via &lt;a href="https://www.trychroma.com/" target="_blank" rel="noopener"&gt;ChromaDB&lt;/a&gt;.&#10;This gives agents searchable memory for preferences, project context, decisions, and recurring facts.&lt;/p&gt;&#10;&lt;p&gt;The second is a file-based memory system inspired by the simplicity of OpenClaw-style agent files.&#10;Instead of burying identity in a database, durable context can live in plain Markdown files that travel with the agent.&#10;That portability matters: I can move the same agent identity from OpenClaw to Hermes to MindRoom without starting from scratch.&lt;/p&gt;&#10;&lt;p&gt;MindRoom also has a memory flush step that periodically extracts durable facts from conversations and writes them back into memory.&#10;So the chat transcript remains chat, while stable preferences, decisions, and identity details become reusable context for future turns.&lt;/p&gt;&#10;&lt;figure id="figure-a-live-personal-memory-demo-mindroom-stores-a-harmless-preference-in-one-turn-and-retrieves-it-later-from-the-same-matrix-conversation"&gt;&lt;img src="https://www.nijho.lt/post/mindroom/mindroom-memory_hu_5090cd21f23bad91.webp" srcset="https://www.nijho.lt/post/mindroom/mindroom-memory_hu_fbc884b2330f0fe3.webp 400w, https://www.nijho.lt/post/mindroom/mindroom-memory_hu_5090cd21f23bad91.webp 760w, https://www.nijho.lt/post/mindroom/mindroom-memory_hu_e6a566cfcdf2d915.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="489" alt="Live MindRoom memory thread showing the generated agent avatars and a public demo where the agent remembers teal as the blog demo accent color." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;A live Personal memory demo: MindRoom stores a harmless preference in one turn and retrieves it later from the same Matrix conversation.&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="6-100-built-in-tool-integrations"&gt;6. 100+ built-in tool integrations &lt;a class="anchor" href="#6-100-built-in-tool-integrations" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Agents can use over 100 built-in integrations:&lt;/p&gt;&#10;&lt;div class="table-wrap"&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;Category&lt;/th&gt;&#10;&lt;th&gt;Examples&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Communication&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;Gmail, Slack, Telegram, Discord&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Development&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;GitHub, Shell, Python, Docker&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Search&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;Tavily, Wikipedia, Arxiv, DuckDuckGo&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Productivity&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;Google Calendar, Jira, Linear, Todoist&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Smart Home&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;Home Assistant&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;AI/ML&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;DALL-E, ElevenLabs, Replicate&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Data&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;Pandas, SQL, DuckDB, Yahoo Finance&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;strong&gt;Web&lt;/strong&gt;&lt;/td&gt;&#10;&lt;td&gt;Firecrawl, Crawl4ai, Browser automation&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Tools are lazy-loaded and credential-managed, so an agent only loads what it needs.&lt;/p&gt;&#10;&lt;h2 id="7-teams-agents-that-collaborate"&gt;7. Teams: agents that collaborate &lt;a class="anchor" href="#7-teams-agents-that-collaborate" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Single agents are useful, but sometimes you need a team.&#10;MindRoom supports two collaboration modes:&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Coordinate mode&lt;/strong&gt;: A lead agent orchestrates others.&#10;You ask a question, the lead delegates subtasks, collects results, and synthesizes a unified response.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Collaborate mode&lt;/strong&gt;: All agents work on the same task in parallel, each providing their independent analysis.&#10;The system then merges their responses with a consensus summary.&lt;/p&gt;&#10;&lt;p&gt;In practice, you might have a research team where one agent searches academic papers, another checks industry news, and a third validates claims—all triggered by a single message.&#10;The live room view below shows a real team response in the same Matrix thread surface normal agents use: threads, routed agents, and room history are all first-class chat objects rather than a separate dashboard.&lt;/p&gt;&#10;&lt;figure id="figure-live-lobby-thread-in-chatmindroomchat-showing-a-real-team-response-from-homeassistant-and-agentbuilder-team-collaboration-uses-the-same-matrix-native-interface-as-regular-agent-chats-with-routed-agents-and-room-membership-visible-beside-the-conversation"&gt;&lt;img src="https://www.nijho.lt/post/mindroom/mindroom-team_hu_afbbb6ff5ac54e3f.webp" srcset="https://www.nijho.lt/post/mindroom/mindroom-team_hu_bf973e9c13f8cf6.webp 400w, https://www.nijho.lt/post/mindroom/mindroom-team_hu_afbbb6ff5ac54e3f.webp 760w, https://www.nijho.lt/post/mindroom/mindroom-team_hu_ee227208566f8d1.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="533" alt="Live MindRoom Lobby thread showing a Team Response from HomeAssistant and AgentBuilder, with the member list showing Super Team and other agents." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Live Lobby thread in chat.mindroom.chat showing a real Team Response from HomeAssistant and AgentBuilder. Team collaboration uses the same Matrix-native interface as regular agent chats, with routed agents and room membership visible beside the conversation.&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="8-hot-reload-change-config-and-plugins-without-downtime"&gt;8. Hot-reload: change config and plugins without downtime &lt;a class="anchor" href="#8-hot-reload-change-config-and-plugins-without-downtime" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;config.yaml&lt;/code&gt; is watched at runtime.&#10;When you edit it—add an agent, change a model, update instructions—MindRoom diffs the old and new config, gracefully restarts only the affected agents, and has them rejoin their rooms.&#10;No downtime, no full runtime restart required.&lt;/p&gt;&#10;&lt;p&gt;The same development loop applies to plugins.&#10;MindRoom has a rich Python hook system, and plugins can be live-developed while the system is running.&#10;Change a plugin file, save it, and MindRoom automatically reloads the plugin without restarting the Python runtime.&lt;/p&gt;&#10;&lt;p&gt;This sounds minor, but when you&amp;rsquo;re iterating on agent behavior or plugin hooks, being able to tweak code and config and see results in seconds significantly improves the development loop.&lt;/p&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/reduced-motion.3dfea0061c17c60a9a200c61ef1e3b3e98d5bdc4eaa4e0a64b3ed2a4d13c72fd.js" integrity="sha256-Pf6gBhwXxgqaIAxh7x47PpjVvcTqpOCmSz7SpNE8cv0=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&lt;video controls autoplay muted playsinline loop preload="metadata"&gt;&#10;&lt;source src="https://www.nijho.lt/post/mindroom/mindroom-hot-reload.mp4" type="video/mp4"&gt;&#10;&lt;/video&gt;&#10;&#10;&lt;h2 id="9-voice-scheduling-and-other-features"&gt;9. Voice, scheduling, and other features &lt;a class="anchor" href="#9-voice-scheduling-and-other-features" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Some features I built purely because I wanted them for myself (a recurring theme in my projects, as anyone who&amp;rsquo;s read my &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;local AI journey&lt;/a&gt; can attest).&#10;I even built &lt;a href="https://github.com/basnijholt/matty" target="_blank" rel="noopener"&gt;Matty&lt;/a&gt;, a Matrix CLI client, from bed at midnight because I needed a way to interact with my agents from the terminal:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Voice messages&lt;/strong&gt;: Audio messages in Matrix are auto-transcribed via Whisper and treated as regular text input.&#10;You can talk to your agents.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Scheduled tasks&lt;/strong&gt;: Natural language scheduling (&lt;code&gt;!schedule &amp;quot;check my email every morning at 9 AM&amp;quot;&lt;/code&gt;) backed by cron jobs.&#10;Agents can run tasks in the background and escalate to you when needed.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;DM support&lt;/strong&gt;: Agents respond naturally in 1:1 conversations without needing mentions.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Cross-organization federation&lt;/strong&gt;: Because Matrix is federated, two companies&amp;rsquo; AI agents can collaborate in a shared room—something that&amp;rsquo;s hard to do on proprietary platforms.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;The voice feature pairs nicely with my &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt; tool—local Whisper transcription on my RTX 3090 means I can talk to my Matrix agents without any cloud dependency.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="10-the-obsession-the-saas-dream-and-the-burnout"&gt;10. The obsession, the SaaS dream, and the burnout &lt;a class="anchor" href="#10-the-obsession-the-saas-dream-and-the-burnout" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I need to be honest about what actually happened with MindRoom, because &amp;ldquo;life got in the way&amp;rdquo; is a sanitized version of the story.&lt;/p&gt;&#10;&lt;p&gt;What really happened is that I got completely, utterly obsessed.&#10;Every single second I wasn&amp;rsquo;t working at my day job or sleeping, I was working on MindRoom.&#10;Many hundreds of hours went into it.&#10;I seriously considered quitting my job and starting an AI startup around it.&lt;/p&gt;&#10;&lt;p&gt;If you&amp;rsquo;ve read Armin Ronacher&amp;rsquo;s post &lt;a href="https://lucumr.pocoo.org/2026/1/18/agent-psychosis/" target="_blank" rel="noopener"&gt;Agent Psychosis: Are We Going Insane?&lt;/a&gt;, you&amp;rsquo;ll know exactly what I&amp;rsquo;m talking about.&#10;The dopamine hit from building with AI agents is incredibly real.&#10;As Armin writes: &amp;ldquo;You feel productive, you feel like everything is amazing, and if you hang out just with people that are into that stuff too, without any checks, you go deeper and deeper into the belief that this all makes perfect sense.&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;That was me.&#10;I was building and building, shipping feature after feature, and it felt incredible.&#10;The codebase grew to over 1,000 commits.&#10;I built the core system, then a React dashboard, then I started on a full SaaS platform—Kubernetes deployments on Hetzner Cloud, a FastAPI backend, a Next.js 15 frontend, Stripe integration, Supabase auth, Helm charts for multi-tenant isolation.&lt;/p&gt;&#10;&lt;p&gt;And that&amp;rsquo;s where the enthusiasm started to erode.&#10;Not because the core idea was bad, but because the work shifted from building interesting things (agent orchestration, memory systems, routing intelligence) to grinding through SaaS boilerplate: GDPR compliance, payment processing, automated Kubernetes deployments, terms of service, cookie banners.&#10;The dopamine loop that kept me going at 2 AM broke when the work stopped being creative and started being compliance paperwork.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;Looking back, I think this is a pattern worth recognizing.&#10;Agentic coding makes it so easy to &lt;em&gt;start&lt;/em&gt; things that you can build yourself into a scope that would normally require a team.&#10;The gap between &amp;ldquo;I can build this&amp;rdquo; and &amp;ldquo;I can maintain and ship this as a product&amp;rdquo; is larger than the tools make it feel.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;p&gt;The hosted control plane lives at &lt;a href="https://app.mindroom.chat" target="_blank" rel="noopener"&gt;app.mindroom.chat&lt;/a&gt;.&#10;I am deliberately not turning this section into a dashboard tour; the interesting part, and the part I still care about most, is the Matrix-native runtime underneath it.&lt;/p&gt;&#10;&lt;h2 id="11-openclaw-and-why-it-matters"&gt;11. OpenClaw and why it matters &lt;a class="anchor" href="#11-openclaw-and-why-it-matters" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://openclaw.ai" target="_blank" rel="noopener"&gt;OpenClaw&lt;/a&gt; takes a different approach to the same core problem.&#10;Where MindRoom builds on Matrix federation as the backbone, OpenClaw runs a local Gateway on your machine and connects to messaging platforms directly (WhatsApp via Baileys, Telegram via grammY, Discord via discord.js, and so on).&lt;/p&gt;&#10;&lt;p&gt;The similarities are striking:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Both give you a single AI assistant across all your messaging platforms&lt;/li&gt;&#10;&lt;li&gt;Both have persistent memory&lt;/li&gt;&#10;&lt;li&gt;Both support multiple tools and integrations&lt;/li&gt;&#10;&lt;li&gt;Both are open-source and privacy-focused&lt;/li&gt;&#10;&lt;li&gt;Both let you choose your own AI model&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The differences are interesting too.&#10;OpenClaw is local-first and TypeScript-based with 380K+ stars and a massive community.&#10;MindRoom is Python-based, federation-first, and&amp;hellip; well, let&amp;rsquo;s just say my GitHub star count is a few orders of magnitude lower.&lt;/p&gt;&#10;&lt;p&gt;But seeing OpenClaw validate the core idea—that people want AI assistants that aren&amp;rsquo;t trapped in apps—reminded me that what I&amp;rsquo;d been building matters.&#10;The federation angle is something OpenClaw doesn&amp;rsquo;t have: the ability for agents from different organizations to collaborate natively, with end-to-end encryption (Olm/Megolm), on a protocol that governments already trust and deploy at scale.&lt;/p&gt;&#10;&lt;figure id="figure-mindroom-uses-matrix-as-the-federated-backbone-openclaw-takes-a-local-gateway-direct-connector-approach"&gt;&lt;img src="https://www.nijho.lt/post/mindroom/mindroom-architecture_hu_aaf71cef609b8340.webp" srcset="https://www.nijho.lt/post/mindroom/mindroom-architecture_hu_2cca5883511aed87.webp 400w, https://www.nijho.lt/post/mindroom/mindroom-architecture_hu_aaf71cef609b8340.webp 760w, https://www.nijho.lt/post/mindroom/mindroom-architecture_hu_afb78d90679bf4a4.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="507" alt="Architecture diagram comparing MindRoom and OpenClaw. MindRoom flows from user to Matrix room to federated homeserver to bridges to messaging platforms. OpenClaw flows from user to local gateway to direct connectors to platform APIs." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;MindRoom uses Matrix as the federated backbone; OpenClaw takes a local-gateway, direct-connector approach.&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;p&gt;And then there&amp;rsquo;s &lt;a href="https://moltbook.com" target="_blank" rel="noopener"&gt;Moltbook&lt;/a&gt;—a social network built exclusively for AI agents that the internet was losing its mind over for a couple of days.&#10;I&amp;rsquo;m not personally convinced it&amp;rsquo;s the next big thing, but what caught my attention is that MindRoom could support something like it &lt;em&gt;natively&lt;/em&gt;.&#10;Matrix is already federated—agents on different servers can already interact, join shared rooms, and collaborate across organizational boundaries.&#10;A bridge from a Reddit-like platform to Matrix and your agents could participate without any special integration.&lt;/p&gt;&#10;&lt;p&gt;Building agent-to-agent infrastructure from scratch also means building security from scratch—and that&amp;rsquo;s where new platforms tend to struggle.&#10;Matrix has spent years hardening its E2E encryption, and it&amp;rsquo;s already deployed by governments and healthcare organizations at scale.&#10;Building on that foundation means you inherit those security properties rather than hoping to get them right yourself.&lt;/p&gt;&#10;&lt;h2 id="12-whats-next"&gt;12. What&amp;rsquo;s next &lt;a class="anchor" href="#12-whats-next" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I first drafted this post a couple of months ago, when MindRoom was still something I was trying to come back to.&#10;That part is already out of date.&#10;Since January I&amp;rsquo;ve been spending nearly every free hour on it again, and since March it has effectively been my full-time work.&#10;The codebase supports 8+ AI model providers—OpenAI, Anthropic, Ollama, Groq, Google, OpenRouter, DeepSeek, Cerebras—and the core architecture is solid.&#10;It also has real users now.&#10;My rough guess is that something like 30-100 people have tried MindRoom in one form or another, which is still early, but meaningfully different from &amp;ldquo;only I have touched this.&amp;rdquo;&#10;There&amp;rsquo;s already a Docker Compose file and a &lt;a href="https://github.com/mindroom-ai/mindroom-stack" target="_blank" rel="noopener"&gt;starter repository&lt;/a&gt; to get started, but I want to make it even simpler.&lt;/p&gt;&#10;&lt;p&gt;What I&amp;rsquo;m focusing on now:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Skills system&lt;/strong&gt;: Building an ecosystem of reusable agent behaviors (already partially implemented with OpenClaw-compatible format)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Even easier onboarding&lt;/strong&gt;: A &lt;code&gt;docker compose up&lt;/code&gt; gets you running today, but I want a wizard that gets you from zero to working agents without touching a YAML file&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Community&lt;/strong&gt;: Turning the small group of early users into a real operator and contributor community&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Scope discipline&lt;/strong&gt;: Keeping the work sustainable now that MindRoom is effectively full-time, not slipping back into the obsessive 2 AM marathon from before&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I&amp;rsquo;m deliberately leaving the SaaS ambitions aside for now.&#10;The core system is what matters, and that&amp;rsquo;s where the energy is going.&lt;/p&gt;&#10;&lt;p&gt;If you&amp;rsquo;re interested in AI agents that live in Matrix and work everywhere, check out &lt;a href="https://github.com/mindroom-ai/mindroom" target="_blank" rel="noopener"&gt;MindRoom on GitHub&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Sometimes seeing someone else succeed with a similar idea is enough to remember why you started building in the first place.&#10;And sometimes the most important lesson from a project isn&amp;rsquo;t technical—it&amp;rsquo;s learning when to step away and when to come back.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;Are you running AI agents on any messaging platforms? Have you tried Matrix for anything beyond regular chat? I&amp;rsquo;d love to hear about your setups!&lt;/em&gt;&lt;/p&gt;&#10;&lt;h2 id="links-and-resources"&gt;Links and resources &lt;a class="anchor" href="#links-and-resources" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/mindroom-ai/mindroom" target="_blank" rel="noopener"&gt;MindRoom on GitHub&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/mindroom-ai/mindroom-stack" target="_blank" rel="noopener"&gt;MindRoom Stack (starter repository)&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://matrix.org/" target="_blank" rel="noopener"&gt;Matrix protocol&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://openclaw.ai" target="_blank" rel="noopener"&gt;OpenClaw&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://mem0.ai/" target="_blank" rel="noopener"&gt;Mem0 (memory system inspiration)&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/agno-agi/agno" target="_blank" rel="noopener"&gt;Agno framework&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/matty" target="_blank" rel="noopener"&gt;Matty (Matrix CLI client)&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://lucumr.pocoo.org/2026/1/18/agent-psychosis/" target="_blank" rel="noopener"&gt;Agent Psychosis: Are We Going Insane?&lt;/a&gt; by Armin Ronacher&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;My agentic coding post&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;My local AI journey&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/agentic-mobile-workflow/"&gt;Coding from my phone&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>Matrix MCP</title><link>https://www.nijho.lt/project/matrix-mcp/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/matrix-mcp/</guid><description/></item><item><title>Privata</title><link>https://www.nijho.lt/project/privata/</link><pubDate>Tue, 05 May 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/privata/</guid><description/></item><item><title>Auto-installing extras at runtime so users never have to think about dependencies</title><link>https://www.nijho.lt/post/auto-install-extras/</link><pubDate>Wed, 11 Feb 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/auto-install-extras/</guid><description>&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-from-one-command-to-a-dependency-maze"&gt;1. From one command to a dependency maze&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-why-just-use-extras-breaks-down"&gt;2. Why &amp;ldquo;just use extras&amp;rdquo; breaks down&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-the-decorator"&gt;3. The decorator&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-checking-without-importing"&gt;4. Checking without importing&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-the-metadata-bridge"&gt;5. The metadata bridge&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-three-environments-three-strategies"&gt;6. Three environments, three strategies&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#a-uv-tool-install--the-persistent-environment"&gt;A) &lt;code&gt;uv tool install&lt;/code&gt; — the persistent environment&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#b-uvx--the-ephemeral-environment"&gt;B) &lt;code&gt;uvx&lt;/code&gt; — the ephemeral environment&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#c-regular-virtualenv-pipx-or-system-install"&gt;C) Regular virtualenv, &lt;code&gt;pipx&lt;/code&gt;, or system install&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#7-preventing-infinite-re-execution-loops"&gt;7. Preventing infinite re-execution loops&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#8-what-users-actually-see"&gt;8. What users actually see&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#9-keeping-everything-in-sync"&gt;9. Keeping everything in sync&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#10-why-uv-made-this-practical"&gt;10. Why uv made this practical&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#11-tradeoffs"&gt;11. Tradeoffs&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#12-not-dependency-injection"&gt;12. Not dependency injection&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#13-conclusion"&gt;13. Conclusion&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-from-one-command-to-a-dependency-maze"&gt;1. From one command to a dependency maze &lt;a class="anchor" href="#1-from-one-command-to-a-dependency-maze" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt; started as a simple thing.&#10;Voice in, text out.&#10;It connected to a &lt;a href="https://github.com/rhasspy/wyoming" target="_blank" rel="noopener"&gt;Wyoming&lt;/a&gt; Whisper server for speech-to-text and a Piper server for text-to-speech, basically a local, open-source Siri triggered from terminal hotkeys.&#10;I wrote about the early version in my &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;local AI journey&lt;/a&gt; post.&lt;/p&gt;&#10;&lt;p&gt;And then, as tends to happen with side projects I use every day, I kept adding things.&lt;/p&gt;&#10;&lt;p&gt;First a RAG proxy with ChromaDB, so I could chat with my documents.&#10;Then a long-term memory system, so conversations could remember context across sessions.&#10;Server commands that run Kokoro TTS or Whisper locally, optimized for whatever hardware you have—Apple Silicon gets MLX, NVIDIA gets CTranslate2.&#10;Voice activity detection with Silero VAD.&#10;Audio speed adjustment.&#10;An &lt;a href="https://www.nijho.lt/post/parallel-agentic-coding/"&gt;agent orchestration system&lt;/a&gt;.&#10;&lt;code&gt;systemd&lt;/code&gt;/&lt;code&gt;launchd&lt;/code&gt; service management for running background daemons.&lt;/p&gt;&#10;&lt;p&gt;Each of these features pulled in wildly different dependency trees.&#10;&lt;code&gt;torch&lt;/code&gt; and &lt;code&gt;transformers&lt;/code&gt; for GPU-accelerated Whisper.&#10;&lt;code&gt;sounddevice&lt;/code&gt; and &lt;code&gt;numpy&lt;/code&gt; for microphone access.&#10;&lt;code&gt;chromadb&lt;/code&gt; and &lt;code&gt;onnxruntime&lt;/code&gt; for vector storage.&#10;&lt;code&gt;kokoro&lt;/code&gt; with its entire spacy pipeline for neural TTS.&lt;/p&gt;&#10;&lt;p&gt;Today the project has &lt;strong&gt;13 optional extras&lt;/strong&gt;.&#10;The tool became much more useful, but dependency UX got progressively worse with every feature.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I moved from &amp;ldquo;users choose extras upfront&amp;rdquo; to &lt;strong&gt;runtime optional dependency resolution&lt;/strong&gt;.&#10;Commands declare required extras with &lt;code&gt;@requires_extras(...)&lt;/code&gt;; if something is missing, &lt;code&gt;agent-cli&lt;/code&gt; installs it in the right environment and re-runs the command automatically.&#10;The user just types &lt;code&gt;agent-cli transcribe&lt;/code&gt; and everything works.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="2-why-just-use-extras-breaks-down"&gt;2. Why &amp;ldquo;just use extras&amp;rdquo; breaks down &lt;a class="anchor" href="#2-why-just-use-extras-breaks-down" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Python&amp;rsquo;s &lt;code&gt;optional-dependencies&lt;/code&gt; in &lt;code&gt;pyproject.toml&lt;/code&gt; is the standard mechanism for this, and it&amp;rsquo;s the right packaging primitive.&#10;You define groups, users install with &lt;code&gt;pip install agent-cli[rag]&lt;/code&gt;.&#10;On paper, this is perfectly reasonable.&lt;/p&gt;&#10;&lt;p&gt;In practice, it falls apart when you have 13 of them:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;optional-dependencies&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;audio&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;numpy&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;sounddevice&amp;gt;=0.4.6&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;agent-cli[wyoming]&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;llm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;pydantic-ai-slim[openai,google,duckduckgo,vertexai]&amp;gt;=0.1.1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;rag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;pydantic-ai-slim[openai,google]&amp;gt;=0.1.1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;chromadb&amp;gt;=0.4.22&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;memory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;pydantic-ai-slim[openai,google]&amp;gt;=0.1.1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;chromadb&amp;gt;=0.4.22&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;vad&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;onnxruntime&amp;gt;=1.16.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;faster-whisper&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;faster-whisper&amp;gt;=1.0.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;mlx-whisper&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;mlx-whisper&amp;gt;=0.4.0; sys_platform == &amp;#39;darwin&amp;#39; and platform_machine == &amp;#39;arm64&amp;#39;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;kokoro&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;kokoro&amp;gt;=0.9.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;soundfile&amp;gt;=0.12.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;piper&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;piper-tts&amp;gt;=1.2.0&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# ... and more&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The UX problem is obvious.&#10;If I want to transcribe voice to text, do I need &lt;code&gt;audio&lt;/code&gt;? &lt;code&gt;llm&lt;/code&gt;? &lt;code&gt;vad&lt;/code&gt;? All three?&#10;What if I want the Whisper server to run locally—is that &lt;code&gt;faster-whisper&lt;/code&gt; or &lt;code&gt;mlx-whisper&lt;/code&gt;?&#10;Do I also need &lt;code&gt;server&lt;/code&gt; for that?&lt;/p&gt;&#10;&lt;p&gt;Expecting users to study a dependency matrix before running a CLI command is a terrible experience.&#10;I wanted three things:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;A lightweight base install that&amp;rsquo;s fast and doesn&amp;rsquo;t pull in heavy ML dependencies.&lt;/li&gt;&#10;&lt;li&gt;Commands that &amp;ldquo;just work&amp;rdquo; when first used.&lt;/li&gt;&#10;&lt;li&gt;A clear opt-out for people who don&amp;rsquo;t want automatic installs.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="3-the-decorator"&gt;3. The decorator &lt;a class="anchor" href="#3-the-decorator" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The core idea is simple.&#10;Each CLI command declares what it needs, and the decorator handles the rest:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@app.command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;transcribe&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rich_help_panel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Voice Commands&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@requires_extras&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;audio&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;llm&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it from the command author&amp;rsquo;s perspective.&#10;One line, and you never think about dependency management in that command again.&lt;/p&gt;&#10;&lt;p&gt;Here are a few more examples from the codebase to show how this scales:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Continuous transcription needs audio, voice activity detection, and an LLM&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@requires_extras&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;audio&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;vad&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;llm&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transcribe_live&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# TTS server needs the server framework + one of two TTS backends + Wyoming protocol&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@requires_extras&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;server&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;piper|kokoro&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;wyoming&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start_tts_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Whisper server: any of three ASR backends will do&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@requires_extras&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;server&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;faster-whisper|mlx-whisper|whisper-transformers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;wyoming&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start_whisper_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Notice the pipe syntax: &lt;code&gt;&amp;quot;piper|kokoro&amp;quot;&lt;/code&gt; means &amp;ldquo;you need &lt;em&gt;either&lt;/em&gt; Piper &lt;em&gt;or&lt;/em&gt; Kokoro—any one of these satisfies the requirement.&amp;rdquo;&#10;On Apple Silicon, if you already have &lt;code&gt;mlx-whisper&lt;/code&gt; installed, it won&amp;rsquo;t try to install &lt;code&gt;faster-whisper&lt;/code&gt; instead.&lt;/p&gt;&#10;&lt;p&gt;The &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/core/deps.py#L365-L383" target="_blank" rel="noopener"&gt;decorator itself&lt;/a&gt; is straightforward:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;requires_extras&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@functools.wraps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_check_and_install_extras&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wrapper&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decorator&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The real complexity lives in &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/core/deps.py#L321-L359" target="_blank" rel="noopener"&gt;&lt;code&gt;_check_and_install_extras&lt;/code&gt;&lt;/a&gt;, which handles several installation scenarios.&#10;But the interface for command authors is always a single line.&lt;/p&gt;&#10;&lt;h2 id="4-checking-without-importing"&gt;4. Checking without importing &lt;a class="anchor" href="#4-checking-without-importing" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;You don&amp;rsquo;t want to &lt;em&gt;import&lt;/em&gt; &lt;code&gt;torch&lt;/code&gt; just to check if it&amp;rsquo;s available—that takes seconds and loads hundreds of megabytes into memory.&#10;Instead, I use &lt;code&gt;importlib.util.find_spec&lt;/code&gt;, which checks whether a module exists on the import path without executing any of its code.&#10;No heavy imports, no GPU initialization, no side effects.&#10;The check is essentially free.&lt;/p&gt;&#10;&lt;h2 id="5-the-metadata-bridge"&gt;5. The metadata bridge &lt;a class="anchor" href="#5-the-metadata-bridge" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;There&amp;rsquo;s a gap between what &lt;code&gt;pyproject.toml&lt;/code&gt; knows and what the runtime check needs.&#10;&lt;code&gt;pyproject.toml&lt;/code&gt; says the &lt;code&gt;rag&lt;/code&gt; extra requires &lt;code&gt;chromadb&amp;gt;=0.4.22&lt;/code&gt;—that&amp;rsquo;s the &lt;em&gt;pip package name&lt;/em&gt;.&#10;But at runtime, I need to check &lt;em&gt;Python import names&lt;/em&gt;.&#10;Sometimes these are the same (&lt;code&gt;chromadb&lt;/code&gt; → &lt;code&gt;import chromadb&lt;/code&gt;), but often they&amp;rsquo;re not: &lt;code&gt;pydantic-ai-slim&lt;/code&gt; is imported as &lt;code&gt;pydantic_ai&lt;/code&gt;, &lt;code&gt;piper-tts&lt;/code&gt; as &lt;code&gt;piper&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;To bridge this, I generate an &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/_extras.json" target="_blank" rel="noopener"&gt;&lt;code&gt;_extras.json&lt;/code&gt;&lt;/a&gt; file that maps each extra to its description and the Python import names needed for the runtime check:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;audio&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Audio recording/playback&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;sounddevice&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;rag&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;RAG proxy (ChromaDB, embeddings)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;chromadb&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pydantic_ai&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;memory&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Long-term memory proxy&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;chromadb&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;yaml&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pydantic_ai&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;vad&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Voice Activity Detection (Silero VAD via ONNX)&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;onnxruntime&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;faster-whisper&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Whisper ASR via CTranslate2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;faster_whisper&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This file is &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/.github/scripts/sync_extras.py" target="_blank" rel="noopener"&gt;generated by a script&lt;/a&gt; that parses &lt;code&gt;pyproject.toml&lt;/code&gt; and applies a mapping table for the package-name-to-import-name conversions.&#10;A pre-commit hook ensures it stays in sync—if you add an extra to &lt;code&gt;pyproject.toml&lt;/code&gt; and forget to update the metadata, the commit fails.&lt;/p&gt;&#10;&lt;p&gt;I wish Python packaging had a standard way to discover a package&amp;rsquo;s top-level import name from its distribution name.&#10;It doesn&amp;rsquo;t, so the mapping table is the least-bad option I found.&lt;/p&gt;&#10;&lt;h2 id="6-three-environments-three-strategies"&gt;6. Three environments, three strategies &lt;a class="anchor" href="#6-three-environments-three-strategies" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The hard part is that people run &lt;code&gt;agent-cli&lt;/code&gt; in several different ways, and each needs a different installation strategy.&lt;/p&gt;&#10;&lt;h3 id="a-uv-tool-install--the-persistent-environment"&gt;A) &lt;code&gt;uv tool install&lt;/code&gt; — the persistent environment &lt;a class="anchor" href="#a-uv-tool-install--the-persistent-environment" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;When installed via &lt;code&gt;uv tool install agent-cli&lt;/code&gt;, the tool lives in a persistent environment under &lt;code&gt;~/.local/share/uv/tools/agent-cli/&lt;/code&gt;.&#10;This environment has a &lt;code&gt;uv-receipt.toml&lt;/code&gt; file that tracks which extras are installed.&lt;/p&gt;&#10;&lt;p&gt;When auto-install triggers, the system reads the current extras from that receipt, merges in the new ones, and reinstalls:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;install_extras_impl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quiet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_uv_tool_install&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;current_extras&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_get_current_uv_tool_extras&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;new_extras&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_extras&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_install_via_uv_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_extras&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quiet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;quiet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So if you already had &lt;code&gt;audio&lt;/code&gt; installed and now run a command that needs &lt;code&gt;llm&lt;/code&gt;, it becomes &lt;code&gt;uv tool install agent-cli[audio,llm] --force&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;The key insight: &lt;code&gt;uv tool upgrade&lt;/code&gt; reads &lt;code&gt;uv-receipt.toml&lt;/code&gt; and preserves the extras list.&#10;Once auto-install adds an extra, it persists across future upgrades.&#10;You never have to install it again.&lt;/p&gt;&#10;&lt;p&gt;(&lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/core/deps.py#L179-L235" target="_blank" rel="noopener"&gt;Full implementation&lt;/a&gt;)&lt;/p&gt;&#10;&lt;h3 id="b-uvx--the-ephemeral-environment"&gt;B) &lt;code&gt;uvx&lt;/code&gt; — the ephemeral environment &lt;a class="anchor" href="#b-uvx--the-ephemeral-environment" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;uvx&lt;/code&gt; is uv&amp;rsquo;s tool for running packages in temporary, disposable environments.&#10;Running &lt;code&gt;uvx agent-cli transcribe&lt;/code&gt; creates a fresh environment, installs &lt;code&gt;agent-cli&lt;/code&gt;, runs the command, and throws the environment away.&lt;/p&gt;&#10;&lt;p&gt;The problem: there&amp;rsquo;s nowhere to persist extras.&#10;No &lt;code&gt;uv-receipt.toml&lt;/code&gt; to update, no virtualenv that survives the invocation.&lt;/p&gt;&#10;&lt;p&gt;The solution is one of my favorite tricks in this system: re-execute the entire command with the extras baked into the &lt;code&gt;uvx&lt;/code&gt; invocation itself:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_maybe_reexec_with_uvx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_REEXEC_MARKER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;_is_uvx_cache&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;uvx_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;which&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;uvx&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;uvx_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;extras_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;,&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extras&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uvx_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--python&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;3.13&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;agent-cli[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;extras_str&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;_maybe_exec_with_marker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Re-running with extras: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;extras_str&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So &lt;code&gt;uvx agent-cli transcribe&lt;/code&gt; transparently becomes &lt;code&gt;uvx agent-cli[audio,llm] transcribe&lt;/code&gt;.&#10;The user sees a brief &amp;ldquo;Re-running with extras: audio, llm&amp;rdquo; message, and then the command works.&lt;/p&gt;&#10;&lt;p&gt;Environment detection is straightforward—&lt;code&gt;uvx&lt;/code&gt; environments live under the uv cache directory:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_is_uvx_cache&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;prefix_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_posix&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/cache/uv/&amp;#34;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prefix_str&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/archive-v&amp;#34;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prefix_str&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(&lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/core/deps.py#L287-L301" target="_blank" rel="noopener"&gt;Full implementation&lt;/a&gt;)&lt;/p&gt;&#10;&lt;h3 id="c-regular-virtualenv-pipx-or-system-install"&gt;C) Regular virtualenv, &lt;code&gt;pipx&lt;/code&gt;, or system install &lt;a class="anchor" href="#c-regular-virtualenv-pipx-or-system-install" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;For plain &lt;code&gt;pip install agent-cli&lt;/code&gt;, &lt;code&gt;pipx&lt;/code&gt;, or virtualenv installs, the system falls back to direct installation.&#10;Each extra has a pre-generated requirements file with pinned versions (e.g., &lt;code&gt;_requirements/audio.txt&lt;/code&gt;), and the install command adapts to whatever&amp;rsquo;s available:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_install_cmd&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;in_venv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_prefix&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;which&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;uv&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;uv&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pip&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;install&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;--python&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;in_venv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;--system&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;-m&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pip&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;install&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;in_venv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;--user&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Prefers &lt;code&gt;uv pip&lt;/code&gt; when available (fast), falls back to regular pip.&#10;Uses &lt;code&gt;--system&lt;/code&gt; or &lt;code&gt;--user&lt;/code&gt; as appropriate when not in a virtualenv.&#10;This is also the path that &lt;code&gt;pipx&lt;/code&gt; installations take—&lt;code&gt;pipx&lt;/code&gt; creates a regular virtualenv under the hood, so the auto-installer detects it as a venv and installs directly into it.&lt;/p&gt;&#10;&lt;p&gt;(&lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/core/deps.py#L215-L248" target="_blank" rel="noopener"&gt;Full implementation&lt;/a&gt;)&lt;/p&gt;&#10;&lt;h2 id="7-preventing-infinite-re-execution-loops"&gt;7. Preventing infinite re-execution loops &lt;a class="anchor" href="#7-preventing-infinite-re-execution-loops" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;There&amp;rsquo;s an obvious footgun here: what if the install succeeds but the package still isn&amp;rsquo;t importable for some reason?&#10;You&amp;rsquo;d get an infinite loop of install → re-exec → check → install → re-exec → &amp;hellip;&lt;/p&gt;&#10;&lt;p&gt;The fix is a simple environment variable marker:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;_REEXEC_MARKER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;_AGENT_CLI_REEXEC&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_maybe_exec_with_marker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_REEXEC_MARKER&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="c1"&gt;# Already re-executed once, don&amp;#39;t loop&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;new_env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;new_env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_REEXEC_MARKER&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execvpe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;os.execvpe&lt;/code&gt; replaces the current process entirely—it never returns.&#10;The marker ensures we only do this once.&#10;If the second run still can&amp;rsquo;t find the packages, it falls back to a helpful error message instead of looping forever.&lt;/p&gt;&#10;&lt;h2 id="8-what-users-actually-see"&gt;8. What users actually see &lt;a class="anchor" href="#8-what-users-actually-see" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;With auto-install &lt;strong&gt;enabled&lt;/strong&gt; (the default), a first run looks like this:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Auto-installing missing extras: audio, llm&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Running: uv tool install agent-cli[audio,llm] --force --python 3.13 -q&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Installation complete!&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Re-running with installed extras...&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And then the command runs normally.&#10;Next time, it runs instantly—no install needed.&lt;/p&gt;&#10;&lt;p&gt;With auto-install &lt;strong&gt;disabled&lt;/strong&gt;, the user gets a clear message telling them exactly what to do:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;This command requires the &amp;#39;audio&amp;#39; extra (Audio recording/playback).&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Install with:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; uv tool install -p 3.13 &amp;#34;agent-cli\[audio]&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; # or&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; agent-cli install-extras audio&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you prefer explicit control, there&amp;rsquo;s also a manual &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/install/extras.py#L24-L104" target="_blank" rel="noopener"&gt;&lt;code&gt;install-extras&lt;/code&gt;&lt;/a&gt; command:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli install-extras rag memory &lt;span class="c1"&gt;# Install specific extras&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli install-extras --all &lt;span class="c1"&gt;# Install everything&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;agent-cli install-extras --list &lt;span class="c1"&gt;# Show available extras with descriptions&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can opt out via environment variable or config file:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;AGENT_CLI_NO_AUTO_INSTALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# ~/.config/agent-cli/config.toml&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;auto_install_extras&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="9-keeping-everything-in-sync"&gt;9. Keeping everything in sync &lt;a class="anchor" href="#9-keeping-everything-in-sync" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The runtime system depends on two generated artifacts:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;_extras.json&lt;/code&gt;&lt;/strong&gt;: maps extras to descriptions and import names (for the &lt;code&gt;find_spec&lt;/code&gt; check)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;_requirements/*.txt&lt;/code&gt;&lt;/strong&gt;: pinned dependency files for each extra (for the pip/uv install)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;code&gt;_extras.json&lt;/code&gt; is generated by &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/.github/scripts/sync_extras.py" target="_blank" rel="noopener"&gt;&lt;code&gt;sync_extras.py&lt;/code&gt;&lt;/a&gt; (run manually or in CI).&#10;The requirements files are generated by &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/.github/scripts/sync_requirements.py" target="_blank" rel="noopener"&gt;&lt;code&gt;sync_requirements.py&lt;/code&gt;&lt;/a&gt;, which runs as a pre-commit hook whenever &lt;code&gt;pyproject.toml&lt;/code&gt; or &lt;code&gt;uv.lock&lt;/code&gt; changes.&#10;A &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/.github/scripts/check_extras_sync.py" target="_blank" rel="noopener"&gt;check script&lt;/a&gt; enforces that &lt;code&gt;_extras.json&lt;/code&gt; stays consistent with &lt;code&gt;pyproject.toml&lt;/code&gt;—if you add an extra but forget to update the metadata, the commit fails with a clear error.&lt;/p&gt;&#10;&lt;p&gt;This means the runtime system never drifts from the source of truth.&#10;&lt;code&gt;pyproject.toml&lt;/code&gt; is the canonical definition, everything else is derived.&lt;/p&gt;&#10;&lt;h2 id="10-why-uv-made-this-practical"&gt;10. Why uv made this practical &lt;a class="anchor" href="#10-why-uv-made-this-practical" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I want to be clear: this system would be &lt;em&gt;much&lt;/em&gt; harder without &lt;a href="https://docs.astral.sh/uv/" target="_blank" rel="noopener"&gt;uv&lt;/a&gt;.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;uv tool install&lt;/code&gt;&lt;/strong&gt; with persistent extras in &lt;code&gt;uv-receipt.toml&lt;/code&gt;—extras survive upgrades automatically&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;uvx&lt;/code&gt;&lt;/strong&gt; for ephemeral execution with extras specified inline&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;uv pip install&lt;/code&gt;&lt;/strong&gt; that&amp;rsquo;s fast enough for runtime auto-install to feel acceptable&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;uv export&lt;/code&gt;&lt;/strong&gt; for generating pinned requirements files from &lt;code&gt;uv.lock&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;With traditional pip, auto-installing at runtime would take 30-60 seconds and feel terrible.&#10;With uv, it typically takes a second.&#10;That&amp;rsquo;s the difference between &amp;ldquo;annoying&amp;rdquo; and &amp;ldquo;barely noticeable.&amp;rdquo;&lt;/p&gt;&#10;&lt;h2 id="11-tradeoffs"&gt;11. Tradeoffs &lt;a class="anchor" href="#11-tradeoffs" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This approach works well, but it&amp;rsquo;s still a tradeoff:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;There&amp;rsquo;s metadata to maintain.&lt;/strong&gt; The package-name-to-import-name mapping requires a lookup table with special cases like &lt;code&gt;piper-tts&lt;/code&gt; → &lt;code&gt;piper&lt;/code&gt; and &lt;code&gt;pydantic-ai-slim&lt;/code&gt; → &lt;code&gt;pydantic_ai&lt;/code&gt;. I wish &lt;code&gt;pyproject.toml&lt;/code&gt; had a field for this.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;The check only verifies importability, not version compatibility.&lt;/strong&gt; &lt;code&gt;find_spec&lt;/code&gt; tells you a module exists, not whether it&amp;rsquo;s the right version. In practice this hasn&amp;rsquo;t been a problem because uv&amp;rsquo;s pinned requirements handle versions, but it&amp;rsquo;s a theoretical gap.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Auto-install can fail in restricted environments.&lt;/strong&gt; Corporate machines, read-only containers, air-gapped systems—the opt-out exists for a reason.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Re-exec is a CLI pattern, not a library pattern.&lt;/strong&gt; &lt;code&gt;os.execvpe&lt;/code&gt; replaces the process, which means any shell hooks or signal handlers set up before the decorator runs are lost. Fine for CLI tools, would be problematic for libraries.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="12-not-dependency-injection"&gt;12. Not dependency injection &lt;a class="anchor" href="#12-not-dependency-injection" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I originally called this &amp;ldquo;dependency injection&amp;rdquo; in my head, but that&amp;rsquo;s the wrong term entirely.&#10;Dependency injection is about providing object dependencies through constructors or function parameters.&#10;This is something different.&lt;/p&gt;&#10;&lt;p&gt;The best names I&amp;rsquo;ve come up with:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;runtime optional dependency resolution&lt;/strong&gt; — accurate but a mouthful&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;on-demand extras installation&lt;/strong&gt; — emphasizes the user experience&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;lazy extras&lt;/strong&gt; — short, probably what I&amp;rsquo;d use in conversation&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;If there&amp;rsquo;s a standard term for this pattern, I haven&amp;rsquo;t found it.&lt;/p&gt;&#10;&lt;h2 id="13-conclusion"&gt;13. Conclusion &lt;a class="anchor" href="#13-conclusion" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The underlying principle is simple: &lt;strong&gt;the tool should know what it needs, and it should get it&lt;/strong&gt;.&#10;Users shouldn&amp;rsquo;t have to study a matrix of 13 optional extras to figure out which pip command to run.&#10;They should just type the command they want, and it should work.&lt;/p&gt;&#10;&lt;p&gt;uv&amp;rsquo;s speed and tooling model made this practical.&#10;A decorator made it ergonomic for command authors.&#10;Pre-commit hooks made it maintainable.&#10;And three environment-detection strategies made it work regardless of how the user installed the tool.&lt;/p&gt;&#10;&lt;p&gt;If you&amp;rsquo;re building a Python CLI with optional features that keep growing, consider this pattern.&#10;Your users will thank you, mostly by never having to ask &amp;ldquo;which extras do I need?&amp;rdquo;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;p&gt;&lt;em&gt;The code discussed here is part of &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt;, an open-source suite of AI-powered command-line tools.&#10;The implementation lives primarily in &lt;a href="https://github.com/basnijholt/agent-cli/blob/c9a67b484331b64733edba8d72d20523699c5a72/agent_cli/core/deps.py" target="_blank" rel="noopener"&gt;&lt;code&gt;agent_cli/core/deps.py&lt;/code&gt;&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;&#10;</description></item><item><title>trueloc</title><link>https://www.nijho.lt/project/trueloc/</link><pubDate>Thu, 15 Jan 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/trueloc/</guid><description/></item><item><title>Compose Farm</title><link>https://www.nijho.lt/project/compose-farm/</link><pubDate>Thu, 11 Dec 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/compose-farm/</guid><description/></item><item><title>SOTA RAG &amp; Memory without the database: Files, Git, and simple folders</title><link>https://www.nijho.lt/post/file-based-rag-memory/</link><pubDate>Wed, 10 Dec 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/file-based-rag-memory/</guid><description>&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-the-amnesia-problem"&gt;1. The amnesia problem&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-learning-from-failure-ai-journal"&gt;2. Learning from failure: AI Journal&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-how-i-studied-sota-clone-read-re-implement"&gt;3. How I studied SOTA: clone, read, re-implement&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-the-dependency-problem-why-onnx-over-pytorch"&gt;4. The dependency problem: why ONNX over PyTorch&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-rag-just-let-me-grep-it-kind-of"&gt;5. RAG: just let me grep it (kind of)&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#the-architecture-openai-compatibility"&gt;The architecture: OpenAI compatibility&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#two-stage-retrieval-the-sota-approach"&gt;Two-stage retrieval: the SOTA approach&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-memory-files-over-databases"&gt;6. Memory: files over databases&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#git-integration-versioning-memory-"&gt;Git integration: versioning memory 🕰️&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-reconciliation-loop"&gt;The reconciliation loop&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-tiny-detail-that-broke-everything"&gt;The tiny detail that broke everything&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#7-why-building-this-was-necessary"&gt;7. Why building this was necessary&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#privacy"&gt;Privacy&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#integration"&gt;Integration&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#dependencies"&gt;Dependencies&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#8-try-it-yourself"&gt;8. Try it yourself&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#try-memory"&gt;Try Memory&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#try-rag"&gt;Try RAG&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#9-what-i-learned"&gt;9. What I learned&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-the-amnesia-problem"&gt;1. The amnesia problem &lt;a class="anchor" href="#1-the-amnesia-problem" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;If you&amp;rsquo;ve read my posts on &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;agentic coding&lt;/a&gt; or my &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;local AI journey&lt;/a&gt;, you know I am all-in on using AI to control my computer and write software.&#10;But one critical piece was missing: &lt;strong&gt;Context&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;LLMs are powerful but stateless.&#10;Every time I start a new chat, the model forgets who I am, what I&amp;rsquo;m working on, and that I prefer &lt;a href="https://www.nijho.lt/post/functional-python/"&gt;functional Python code&lt;/a&gt; over object-oriented styles.&#10;Worse, it also forgets the thing I just told it five minutes ago in another window.&lt;/p&gt;&#10;&lt;p&gt;The standard solution to this is &lt;a href="https://en.wikipedia.org/wiki/Retrieval-augmented_generation" target="_blank" rel="noopener"&gt;RAG (Retrieval-Augmented Generation)&lt;/a&gt; and &lt;a href="https://arxiv.org/abs/2310.08560" target="_blank" rel="noopener"&gt;Memory&lt;/a&gt;.&#10;But when I looked at the available tools, I hit a wall.&lt;/p&gt;&#10;&lt;p&gt;Most solutions fell into two buckets:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;SaaS / Web UIs:&lt;/strong&gt; Upload your files to a cloud service or use a specific web chat interface that locks you in.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Developer Libraries:&lt;/strong&gt; &amp;ldquo;Just use LangChain/LlamaIndex!&amp;rdquo; they say. Great, now I have to write a Python script every time I want to ask a question about a PDF?&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;I didn&amp;rsquo;t want a library. I didn&amp;rsquo;t want a web UI.&#10;&lt;strong&gt;I wanted a folder.&lt;/strong&gt;&#10;I wanted to drop a file into &lt;code&gt;~/my-docs&lt;/code&gt;, and have my AI instantly know about it.&#10;And I wanted my AI&amp;rsquo;s &amp;ldquo;memory&amp;rdquo; to be a file I can edit, not a hidden vector in a database I can&amp;rsquo;t inspect.&lt;/p&gt;&#10;&lt;p&gt;So I decided to build my own file-based solution.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt; features that combine two ideas:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;File-based storage:&lt;/strong&gt; Documents in a folder, memories as Markdown files with Git versioning.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;OpenAI-compatible proxy:&lt;/strong&gt; Works with &lt;em&gt;any&lt;/em&gt; OpenAI-compatible tool—Cursor, Cline, Open WebUI, LibreChat, Lobe Chat, your terminal—without lock-in.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Tools like Open WebUI have built-in RAG, but that forces you to use only Open WebUI.&#10;My proxy means I can switch tools whenever I want, and they all share the same memory and document index.&lt;/p&gt;&#10;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="2-learning-from-failure-ai-journal"&gt;2. Learning from failure: AI Journal &lt;a class="anchor" href="#2-learning-from-failure-ai-journal" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This isn&amp;rsquo;t my first attempt at building a memory system.&lt;/p&gt;&#10;&lt;p&gt;A few months ago, I built &lt;a href="https://github.com/basnijholt/aijournal" target="_blank" rel="noopener"&gt;&lt;strong&gt;AI Journal&lt;/strong&gt;&lt;/a&gt;—an ambitious local-first journaling app where an AI would read your entries and build a &amp;ldquo;living self-model&amp;rdquo; of who you are.&#10;It had sophisticated features: a structured &amp;ldquo;persona core,&amp;rdquo; typed &amp;ldquo;claim atoms&amp;rdquo; with evidence links, four organizational layers (L1→L4), recency-aware scoring with decay functions, and Git-based time travel to &amp;ldquo;ask questions of your younger self.&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;The architecture seemed solid.&#10;The problem was simpler: &lt;strong&gt;local models aren&amp;rsquo;t good enough yet.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;I designed the whole thing to run entirely on Ollama with my dual RTX 3090s.&#10;But even with a 48GB of VRAM, the models couldn&amp;rsquo;t reliably extract structured claims, handle the multi-step reconciliation, or maintain coherence across long conversations.&#10;Whether the design was right, I still don&amp;rsquo;t know—but the execution definitely hit the wall of current local LLM capabilities.&lt;/p&gt;&#10;&lt;p&gt;The lesson: I tried to build the entire system at once without validating the core concepts and individual ideas first.&#10;This time, I took a different approach: &lt;strong&gt;tackle it problem by problem.&lt;/strong&gt;&#10;Instead of designing a complete system upfront, I would validate each piece—chunking, retrieval, re-ranking, memory reconciliation—before combining them.&#10;The experience and UX would be my own, but the algorithms would be adapted from established projects.&lt;/p&gt;&#10;&lt;h2 id="3-how-i-studied-sota-clone-read-re-implement"&gt;3. How I studied SOTA: clone, read, re-implement &lt;a class="anchor" href="#3-how-i-studied-sota-clone-read-re-implement" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My approach was more methodical this time.&#10;I cloned five major frameworks into the same workspace as &lt;code&gt;agent-cli&lt;/code&gt;:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/run-llama/llama_index" target="_blank" rel="noopener"&gt;LlamaIndex&lt;/a&gt;&lt;/strong&gt;: The OG RAG framework&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/langchain-ai/langchain" target="_blank" rel="noopener"&gt;LangChain&lt;/a&gt;&lt;/strong&gt;: The kitchen-sink approach&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/letta-ai/letta" target="_blank" rel="noopener"&gt;Letta&lt;/a&gt;&lt;/strong&gt; (formerly MemGPT): Stateful agents with long-term memory&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/mem0ai/mem0" target="_blank" rel="noopener"&gt;Mem0&lt;/a&gt;&lt;/strong&gt;: Focused specifically on memory extraction&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/pydantic/pydantic-ai" target="_blank" rel="noopener"&gt;PydanticAI&lt;/a&gt;&lt;/strong&gt;: Clean, typed agent framework&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I didn&amp;rsquo;t read through all this code myself—that would take months.&#10;Instead, I used AI coding agents to search the codebases and write summaries of the technical implementation details:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Gemini CLI&lt;/strong&gt; (Gemini 3.0 Pro)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; (Opus 4.5)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Codex CLI&lt;/strong&gt; (GPT-5.1-Codex-Max)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I fed those summaries into &lt;strong&gt;ChatGPT 5 Pro&lt;/strong&gt; together with my high level plan of what I wanted to create, and it created an implementation plan.&#10;Then I took that plan back to the coding agents for implementation.&#10;I personally found Opus 4.5 the best coding model, but GPT‑5.1 Codex Max was the best at reviewing the code and actually identifying critical bugs.&lt;/p&gt;&#10;&lt;p&gt;For validation, I used &lt;a href="https://github.com/basnijholt/clip-files" target="_blank" rel="noopener"&gt;&lt;code&gt;clip-files&lt;/code&gt;&lt;/a&gt; to copy the agent-cli source code plus the architecture documents (like &lt;a href="https://github.com/basnijholt/agent-cli/blob/main/docs/architecture/memory.md" target="_blank" rel="noopener"&gt;memory.md&lt;/a&gt; and &lt;a href="https://github.com/basnijholt/agent-cli/blob/main/docs/architecture/rag.md" target="_blank" rel="noopener"&gt;rag.md&lt;/a&gt;) into ChatGPT 5 Pro to validate the approach.&lt;/p&gt;&#10;&lt;p&gt;The AI did the searching and implementing; I did the understanding and decision-making.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; Existing frameworks are incredibly over-engineered for my use case, but the &lt;em&gt;core algorithms&lt;/em&gt; are gold.&#10;My job was to extract the essence and re-implement it cleanly.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="4-the-dependency-problem-why-onnx-over-pytorch"&gt;4. The dependency problem: why ONNX over PyTorch &lt;a class="anchor" href="#4-the-dependency-problem-why-onnx-over-pytorch" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;One thing became immediately clear when going through these frameworks: &lt;strong&gt;they&amp;rsquo;re massive&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;Installing LlamaIndex with all its dependencies pulls in PyTorch.&#10;PyTorch alone is &lt;strong&gt;2-3GB&lt;/strong&gt;.&#10;Add transformers, sentence-transformers, and the kitchen sink, and you&amp;rsquo;re looking at an &lt;strong&gt;8GB+ environment&lt;/strong&gt; just to ask questions about some PDFs.&lt;/p&gt;&#10;&lt;p&gt;This felt excessive for a CLI tool.&lt;/p&gt;&#10;&lt;p&gt;One of my core design goals was &lt;strong&gt;minimal dependencies&lt;/strong&gt;.&#10;I wanted &lt;code&gt;agent-cli&lt;/code&gt; to install in seconds, not minutes.&#10;I wanted it to work on my Mac, my NixOS server, and my friend&amp;rsquo;s laptop without downloading half the internet.&lt;/p&gt;&#10;&lt;p&gt;The solution: &lt;strong&gt;ONNX Runtime&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;ONNX Runtime is ~200MB and runs inference on basically anything—CPU, CUDA, even Apple Silicon.&#10;For embeddings, I use &lt;a href="https://developers.googleblog.com/introducing-embeddinggemma/" target="_blank" rel="noopener"&gt;&lt;code&gt;embeddinggemma&lt;/code&gt;&lt;/a&gt; running locally via &lt;a href="https://www.nijho.lt/post/llama-nixos/"&gt;llama.cpp&lt;/a&gt;.&#10;For re-ranking, I use a cross-encoder exported to ONNX (&lt;code&gt;Xenova/ms-marco-MiniLM-L-6-v2&lt;/code&gt;).&lt;/p&gt;&#10;&lt;p&gt;The result: &lt;code&gt;pip install agent-cli[rag]&lt;/code&gt; takes about 30 seconds and adds ~300MB.&#10;The key insight: embeddings and re-ranking are cheap operations.&#10;You don&amp;rsquo;t need a GPU for them—CPU is perfectly fine.&#10;PyTorch with CUDA support can easily reach 2-3GB+; ONNX Runtime is ~200MB and runs anywhere.&#10;Since I&amp;rsquo;m not training models, just running inference on small encoders, ONNX is the obvious choice.&lt;/p&gt;&#10;&lt;h2 id="5-rag-just-let-me-grep-it-kind-of"&gt;5. RAG: just let me grep it (kind of) &lt;a class="anchor" href="#5-rag-just-let-me-grep-it-kind-of" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My requirement for RAG was simple: &lt;strong&gt;Files on disk are the source of truth.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;I didn&amp;rsquo;t want to manage an ingestion pipeline or trigger APIs.&#10;I implemented a &lt;a href="https://github.com/basnijholt/agent-cli/blob/main/docs/architecture/rag.md" target="_blank" rel="noopener"&gt;RAG proxy&lt;/a&gt; that uses OS-level file watchers (&lt;code&gt;watchfiles&lt;/code&gt;).&#10;Here is the workflow:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;I drop a PDF, Markdown, or Python file into &lt;code&gt;~/rag_docs&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;The system detects the &lt;code&gt;Create&lt;/code&gt; event.&lt;/li&gt;&#10;&lt;li&gt;It chunks the text and embeds it into a local ChromaDB instance.&lt;/li&gt;&#10;&lt;li&gt;It&amp;rsquo;s immediately searchable.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;When moving or deleting files, the system updates the index accordingly.&lt;/p&gt;&#10;&lt;h3 id="the-architecture-openai-compatibility"&gt;The architecture: OpenAI compatibility &lt;a class="anchor" href="#the-architecture-openai-compatibility" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;The &amp;ldquo;secret sauce&amp;rdquo; isn&amp;rsquo;t the retrieval itself; it&amp;rsquo;s how I expose it.&#10;Instead of building a custom chat app, I built an &lt;strong&gt;OpenAI-compatible proxy&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;div class="mermaid"&gt;flowchart LR&#10; Client["Client&lt;br/&gt;(Cursor, Open WebUI)"] --&gt; Retrieve&#10;&#10; subgraph Proxy ["RAG Proxy"]&#10; direction TB&#10; Retrieve --&gt; Augment["Augment &amp; Forward"]&#10; Chroma[("ChromaDB")] -.-&gt; Retrieve&#10; Watcher["File Watcher"] --&gt; Chroma&#10; end&#10;&#10; Augment --&gt; Upstream["Upstream LLM"]&lt;/div&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Point any OpenAI-compatible tool at your local proxy&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000/v1&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it. Any tool that supports custom OpenAI endpoints—Cursor, Cline, Open WebUI, LibreChat—instantly gains the ability to &amp;ldquo;read&amp;rdquo; my document folder.&lt;/p&gt;&#10;&lt;h3 id="two-stage-retrieval-the-sota-approach"&gt;Two-stage retrieval: the SOTA approach &lt;a class="anchor" href="#two-stage-retrieval-the-sota-approach" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;I looked at LlamaIndex here.&#10;Standard &amp;ldquo;semantic search&amp;rdquo; (using just embeddings/bi-encoders) is often disappointing.&#10;It finds text that &lt;em&gt;looks&lt;/em&gt; similar but isn&amp;rsquo;t actually relevant.&lt;/p&gt;&#10;&lt;p&gt;A common pattern in modern RAG systems is &lt;strong&gt;Two-Stage Retrieval&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Fast Stage:&lt;/strong&gt; Retrieve 3x candidates using standard embeddings (&lt;code&gt;text-embedding-3-small&lt;/code&gt; or local models like &lt;a href="https://developers.googleblog.com/introducing-embeddinggemma/" target="_blank" rel="noopener"&gt;&lt;code&gt;embeddinggemma&lt;/code&gt;&lt;/a&gt;).&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Smart Stage:&lt;/strong&gt; Use a &lt;strong&gt;Cross-Encoder&lt;/strong&gt; to re-rank the results.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;A cross-encoder looks at the query and the document &lt;em&gt;together&lt;/em&gt; and outputs a relevance score.&#10;It&amp;rsquo;s computationally heavier, but since I&amp;rsquo;m running this locally on my RTX 3090 (or even CPU via ONNX), the 50ms latency penalty is worth it for results that are actually useful.&lt;/p&gt;&#10;&lt;p&gt;This isn&amp;rsquo;t a new idea, but implementing it cleanly with ONNX avoided the huge dependency tax.&lt;/p&gt;&#10;&lt;h2 id="6-memory-files-over-databases"&gt;6. Memory: files over databases &lt;a class="anchor" href="#6-memory-files-over-databases" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This is where my approach diverges most from standard tools like Mem0.&#10;Most memory systems treat memories as database rows.&#10;If the AI remembers &amp;ldquo;User likes pizza,&amp;rdquo; that&amp;rsquo;s a vector in a DB.&#10;If it hallucinates and records &amp;ldquo;User hates pizza,&amp;rdquo; good luck fixing it.&#10;You need to write a script to query the DB and delete the row.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;I prefer data I can inspect.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;My memory system stores every fact as a &lt;strong&gt;Markdown file&lt;/strong&gt; with YAML front matter.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-markdown" data-lang="markdown"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;550e8400-e29b-41d4-a716-446655440000&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;memory&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;2025-12-15T10:00:00Z&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;conversation_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;default&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nn"&gt;---&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;The user prefers using &amp;#39;uv&amp;#39; for Python package management.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="git-integration-versioning-memory-"&gt;Git integration: versioning memory 🕰️ &lt;a class="anchor" href="#git-integration-versioning-memory-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Because memories are just files, I added a feature that I haven&amp;rsquo;t seen anywhere else: &lt;strong&gt;Automatic Git Versioning&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;Every time the AI learns a new fact, updates an old one, or deletes a contradiction, the system automatically commits the change to a local git repository.&lt;/p&gt;&#10;&lt;p&gt;This leads to incredible capabilities:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Inspectability:&lt;/strong&gt; I can &lt;code&gt;git log&lt;/code&gt; and see exactly &lt;em&gt;when&lt;/em&gt; the AI learned something.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Correction:&lt;/strong&gt; If the AI learns something wrong, I open the markdown file in VS Code, edit it, and save.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Reversion:&lt;/strong&gt; Agent hallucinated wildly yesterday? &lt;code&gt;git reset --hard HEAD~1&lt;/code&gt;.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="the-reconciliation-loop"&gt;The reconciliation loop &lt;a class="anchor" href="#the-reconciliation-loop" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;The logic for storing memories isn&amp;rsquo;t just &amp;ldquo;append to file.&amp;rdquo;&#10;Digging into Mem0 (and Letta) showed me that memory systems need active &lt;strong&gt;reconciliation&lt;/strong&gt;.&#10;When I say &amp;ldquo;Actually, I switched to nix instead of uv,&amp;rdquo; the system:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Retrieves&lt;/strong&gt; existing memories about package managers.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Decides&lt;/strong&gt; (using a lightweight LLM) whether to &lt;code&gt;ADD&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, or &lt;code&gt;DELETE&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Executes&lt;/strong&gt; the file operation (moving the old fact to a &lt;code&gt;deleted/&lt;/code&gt; folder for audit trails).&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;This keeps the memory bank clean and contradictory-free, unlike simple vector stores that just accumulate conflicting junk over time.&lt;/p&gt;&#10;&lt;h3 id="the-tiny-detail-that-broke-everything"&gt;The tiny detail that broke everything &lt;a class="anchor" href="#the-tiny-detail-that-broke-everything" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;After implementing the memory system—mostly modeled after Mem0—I noticed it didn&amp;rsquo;t work as well.&#10;After a lot of debugging, I found a tiny difference: I was asking the model for structured data via a tool call, whereas Mem0 uses freeform JSON that the LLM returns directly.&lt;/p&gt;&#10;&lt;p&gt;Since I use PydanticAI, fixing this was a one-line change:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model_cfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UPDATE_MEMORY_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MemoryDecision&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;output_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PromptedOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MemoryDecision&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="c1"&gt;# JSON mode instead of tool calls&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is exactly why reading the source code matters—not just the algorithms, but the tiny details that make them work.&lt;/p&gt;&#10;&lt;h2 id="7-why-building-this-was-necessary"&gt;7. Why building this was necessary &lt;a class="anchor" href="#7-why-building-this-was-necessary" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;You might ask: &amp;ldquo;Bas, why write thousands of lines of code for this? Why not just use ChatGPT&amp;rsquo;s memory or install LangChain?&amp;rdquo;&lt;/p&gt;&#10;&lt;p&gt;Three reasons: &lt;strong&gt;Privacy&lt;/strong&gt;, &lt;strong&gt;Integration&lt;/strong&gt;, and &lt;strong&gt;Dependencies&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;h3 id="privacy"&gt;Privacy &lt;a class="anchor" href="#privacy" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;I want to feed my AI my financial records, my health data, and my private journals.&#10;I am not comfortable sending that to OpenAI&amp;rsquo;s long-term storage or some VC-backed startup&amp;rsquo;s vector cloud.&#10;With &lt;code&gt;agent-cli&lt;/code&gt;, the vectors live in &lt;code&gt;~/.cache/agent-cli/chroma&lt;/code&gt;, the files live in &lt;code&gt;~/documents&lt;/code&gt;, and the only thing leaving my machine is the specific context needed for a specific query (if I use a cloud model) or nothing at all (if I use Ollama/&lt;a href="https://www.nijho.lt/post/llama-nixos/"&gt;llama.cpp&lt;/a&gt;).&lt;/p&gt;&#10;&lt;h3 id="integration"&gt;Integration &lt;a class="anchor" href="#integration" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;By building this as an API proxy, I solved the &amp;ldquo;fragmented tools&amp;rdquo; problem.&#10;Any tool that supports custom OpenAI-compatible endpoints works out of the box:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;agent-cli chat&lt;/code&gt;&lt;/strong&gt; in the terminal&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Cursor&lt;/strong&gt; or &lt;strong&gt;Cline&lt;/strong&gt; for agentic coding&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;a href="https://openwebui.com/" target="_blank" rel="noopener"&gt;Open WebUI&lt;/a&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;a href="https://www.librechat.ai/" target="_blank" rel="noopener"&gt;LibreChat&lt;/a&gt;&lt;/strong&gt;, or &lt;strong&gt;&lt;a href="https://lobechat.com/" target="_blank" rel="noopener"&gt;Lobe Chat&lt;/a&gt;&lt;/strong&gt; in the browser&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Because my system speaks &amp;ldquo;OpenAI,&amp;rdquo; all of these tools share the &lt;em&gt;same&lt;/em&gt; memory and the &lt;em&gt;same&lt;/em&gt; document index.&#10;If I tell the terminal agent &amp;ldquo;I&amp;rsquo;m working on Project X,&amp;rdquo; and then switch to Cursor, Cursor knows about Project X.&#10;I can try a new chat UI tomorrow without losing anything—just point it to &lt;code&gt;localhost:8000&lt;/code&gt;.&lt;/p&gt;&#10;&lt;h3 id="dependencies"&gt;Dependencies &lt;a class="anchor" href="#dependencies" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;To me, it just doesn&amp;rsquo;t feel right that asking questions about a PDF requires downloading 8GB of PyTorch.&#10;By choosing ONNX Runtime over the PyTorch ecosystem, I kept the entire install lightweight and fast.&#10;This matters when you want to run the system on a cheap VPS or your friend&amp;rsquo;s laptop.&lt;/p&gt;&#10;&lt;h2 id="8-try-it-yourself"&gt;8. Try it yourself &lt;a class="anchor" href="#8-try-it-yourself" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The entire system is open-source and available in &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h3 id="try-memory"&gt;Try Memory &lt;a class="anchor" href="#try-memory" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Get an LLM that remembers you using &lt;a href="https://ollama.com" target="_blank" rel="noopener"&gt;Ollama&lt;/a&gt;.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 1. Pull models and start proxy&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ollama pull embeddinggemma:300m &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ollama pull gpt-oss:20b&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uvx -p 3.13 --from &lt;span class="s2"&gt;&amp;#34;agent-cli[memory]&amp;#34;&lt;/span&gt; agent-cli memory proxy &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --memory-path ./my-memories &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --openai-base-url http://localhost:11434/v1 &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --embedding-model embeddinggemma:300m&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 2. Chat from the terminal (env vars point to proxy)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8100/v1 &lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dummy&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Tell it something about yourself&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uvx openai api chat.completions.create -m gpt-oss:20b -g user &lt;span class="s2"&gt;&amp;#34;My name is Alice and I love hiking&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Later, ask if it remembers&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uvx openai api chat.completions.create -m gpt-oss:20b -g user &lt;span class="s2"&gt;&amp;#34;What&amp;#39;s my name?&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="try-rag"&gt;Try RAG &lt;a class="anchor" href="#try-rag" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Chat with your documents.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 1. Start the RAG proxy (runs in foreground)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uvx -p 3.13 --from &lt;span class="s2"&gt;&amp;#34;agent-cli[rag]&amp;#34;&lt;/span&gt; agent-cli rag-proxy &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --docs-folder ./my-docs &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --openai-base-url http://localhost:11434/v1 &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --embedding-model embeddinggemma:300m&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 2. Chat with your docs&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;OPENAI_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8000/v1 &lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dummy &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; uvx openai api chat.completions.create &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -m gpt-oss:20b &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -g user &lt;span class="s2"&gt;&amp;#34;What does my documentation say about X?&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="9-what-i-learned"&gt;9. What I learned &lt;a class="anchor" href="#9-what-i-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This journey reinforced a few beliefs:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Validate components first.&lt;/strong&gt; AI Journal failed not because the ideas were wrong, but because I tried to build a complete system before validating individual components. This time I tackled problems one at a time—chunking, retrieval, re-ranking, memory reconciliation—and only combined them once each piece worked.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Study implementations, not just papers.&lt;/strong&gt; Reading about two-stage retrieval is one thing. Reading how LlamaIndex actually implements it—the edge cases, the defaults, the performance tradeoffs—is far more valuable. Clone the repos. Read (or make AI read) the code.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Dependencies matter.&lt;/strong&gt; A 30-second install versus a 10-minute download changes whether someone actually tries your tool. I am happy with the ONNX Runtime over PyTorch choice.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Files are the ultimate API.&lt;/strong&gt; By stripping away the complex databases and proprietary interfaces, I ended up with a system that is robust, version-controllable, and surprisingly simple to reason about.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Use the best model for each job.&lt;/strong&gt; I used 4 different AI models during development, each with different strengths. Gemini 3.0 Pro, Opus 4.5, GPT-5.1-Codex Max. I used all of them during the implementation phase, but each one has its own strengths. Fore example, Opus 4.5 was the best coding model, but GPT-5.1 Codex Max was the best at reviewing the code and actually identifying critical bugs (use the built-in &lt;code&gt;/review&lt;/code&gt; feature).&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;</description></item><item><title>Wyoming MLX Whisper</title><link>https://www.nijho.lt/project/wyoming-mlx-whisper/</link><pubDate>Fri, 05 Dec 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/wyoming-mlx-whisper/</guid><description/></item><item><title>GitHub Backup Sync</title><link>https://www.nijho.lt/project/github-backup-sync/</link><pubDate>Fri, 24 Oct 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/github-backup-sync/</guid><description/></item><item><title>Matty</title><link>https://www.nijho.lt/project/matty/</link><pubDate>Sat, 23 Aug 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/matty/</guid><description/></item><item><title>MindRoom</title><link>https://www.nijho.lt/project/mindroom/</link><pubDate>Tue, 29 Jul 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/mindroom/</guid><description/></item><item><title>How a 3090 turned into two local‑first AI projects 🎮</title><link>https://www.nijho.lt/post/local-ai-journey/</link><pubDate>Mon, 14 Jul 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/local-ai-journey/</guid><description>&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-introduction-the-unplayed-games"&gt;1. Introduction: The Unplayed Games&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-the-rocky-road-to-a-working-setup"&gt;2. The Rocky Road to a Working Setup&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-the-first-victory-agent-cli-"&gt;3. The First Victory: &lt;code&gt;agent-cli&lt;/code&gt; 🐍&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-the-grand-ambition-aibrain-"&gt;4. The Grand Ambition: &lt;code&gt;AIBrain&lt;/code&gt; 🧠&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-conclusion-the-journey-continues"&gt;5. Conclusion: The Journey Continues&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-introduction-the-unplayed-games"&gt;1. Introduction: The Unplayed Games &lt;a class="anchor" href="#1-introduction-the-unplayed-games" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I&amp;rsquo;ve wanted to dabble with local AI for a long time, mainly because of the possibilities it unlocks.&#10;Local AI allows me to use all of my data—financial records, personal messages, location history, even transcribing everything I say—without the privacy concerns of sending it to a cloud provider.&#10;This level of deep, personal data analysis is only something I&amp;rsquo;d be comfortable with on my own hardware.&lt;/p&gt;&#10;&lt;p&gt;However, I couldn&amp;rsquo;t justify buying a powerful GPU just for an imagined AI project that I might never really start.&#10;Then it hit me: I could also use it for gaming (duh).&#10;Because I really enjoyed &lt;em&gt;The Last of Us&lt;/em&gt; series, I thought, “&lt;em&gt;Okay, why not play &lt;strong&gt;The Last of Us&lt;/strong&gt; on PC?&lt;/em&gt;”&#10;This was the push I needed.&#10;So, a couple of weeks ago, I convinced myself to buy someone&amp;rsquo;s old gaming machine.&#10;After many deep research sessions with ChatGPT about the best bang-for-the-buck consumer-grade GPU, I landed on an NVIDIA 3090.&#10;I found an (old) beast of a machine, and my initial idea was set.&lt;/p&gt;&#10;&lt;details&gt;&#10; &lt;summary&gt;Click here to see the full system specs ($1350 for all this!)&lt;/summary&gt;&#10; &lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;GPU:&lt;/strong&gt; ASUS ROG STRIX RTX 3090 OC (24GB)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;CPU:&lt;/strong&gt; AMD Ryzen 9 3900X (12-Core)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Motherboard:&lt;/strong&gt; ASUS ROG Crosshair VIII Hero&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Power Supply:&lt;/strong&gt; Be Quiet! 1200W Dark Power Pro&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;RAM:&lt;/strong&gt; 32GB G.Skill Trident Z Royal DDR4&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Storage:&lt;/strong&gt; 2TB Intel M.2 NVMe SSD&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Case:&lt;/strong&gt; Corsair 4000D Airflow&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;CPU Cooler:&lt;/strong&gt; Thermalright Peerless Assassin 120&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;/details&gt;&#10;&#10;&lt;p&gt;Fast forward four weeks.&#10;I haven&amp;rsquo;t played a single minute, but my machine is now indexing 200 GB of email archives and facing a serious performance bottleneck I&amp;rsquo;ll get to later.&#10;That &amp;ldquo;gaming&amp;rdquo; machine has been humming away, but not rendering virtual worlds.&#10;It&amp;rsquo;s been training, transcribing, and thinking.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m writing this post simply because I am so enthusiastic about local AI.&#10;I&amp;rsquo;ve found that local models are surprisingly powerful for specific use cases.&#10;Of course, they aren&amp;rsquo;t going to replace Gemini or Claude for coding, but for many other applications, they work incredibly well.&#10;Lately, I&amp;rsquo;ve been so obsessed with this that I&amp;rsquo;m sleeping significantly less and spending all my free hours working on it.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; This post is the story of how a quest for gaming turned into a deep dive into local, private AI.&#10;I&amp;rsquo;ll share the two open-source Python packages that came out of it, &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/basnijholt/aibrain" target="_blank" rel="noopener"&gt;&lt;code&gt;AIBrain&lt;/code&gt;&lt;/a&gt;, and the lessons I learned along the way.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="2-the-rocky-road-to-a-working-setup"&gt;2. The Rocky Road to a Working Setup &lt;a class="anchor" href="#2-the-rocky-road-to-a-working-setup" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Before I could even get to the fun AI stuff, I had to wrestle with the machine itself.&#10;I started with Pop!_OS, which is supposed to be a great out of the box solution that &amp;ldquo;just works&amp;rdquo;.&#10;However, it&amp;rsquo;s been a long time since I used Linux on the desktop; my experience is almost exclusively with servers.&#10;I quickly installed some of the wrong NVIDIA drivers and ended up debugging stuff in Grub.&#10;I got frustrated with the different commands I had to run to set up everything and realized that reproducing this system was going to be a massive pain.&#10;Since I&amp;rsquo;ve grown to love Nix on my Mac with Nix-Darwin, I decided to switch the whole system to &lt;strong&gt;NixOS&lt;/strong&gt;.&#10;This has been an amazing decision.&#10;Even though there are many pain points with Nix, I think it is well worth it.&#10;For those interested, you can see the complete setup, including all the local AI services, in my &lt;a href="https://github.com/basnijholt/dotfiles/blob/841cd5fb94b65556e7a2cc7d8ea618f86b50bf1d/configs/nixos/configuration.nix" target="_blank" rel="noopener"&gt;NixOS configuration on GitHub&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="3-the-first-victory-agent-cli-"&gt;3. The First Victory: &lt;code&gt;agent-cli&lt;/code&gt; 🐍 &lt;a class="anchor" href="#3-the-first-victory-agent-cli-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;With a stable system, I quickly got started with a few scripts.&#10;These soon evolved into my first real project of this adventure: &lt;a href="https://github.com/basnijholt/agent-cli" target="_blank" rel="noopener"&gt;&lt;code&gt;agent-cli&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;agent-cli&lt;/code&gt; is a collection of &lt;strong&gt;local-first&lt;/strong&gt;, AI-powered command-line agents that run entirely on your machine.&#10;The core philosophy is privacy; your data never leaves your computer.&#10;It&amp;rsquo;s designed for seamless integration with system-wide hotkeys.&#10;I&amp;rsquo;m now using it constantly.&lt;/p&gt;&#10;&lt;p&gt;Instead of typing, I now dictate almost everything.&#10;A quick hotkey, and &lt;code&gt;agent-cli transcribe&lt;/code&gt; turns my speech into text.&#10;Another hotkey, and &lt;code&gt;agent-cli autocorrect&lt;/code&gt; cleans it up using a local LLM running on &lt;strong&gt;Ollama&lt;/strong&gt;.&#10;It has genuinely streamlined my workflow.&#10;For example, I can pipe the tools together, like correcting text from my clipboard and then having it spoken aloud: &lt;code&gt;agent-cli autocorrect | agent-cli speak&lt;/code&gt;.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;p&gt;&lt;strong&gt;⚡ Surprising Speed: Local vs. Cloud&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;One of my biggest surprises was the raw speed of local models.&#10;While I added an OpenAI provider to &lt;code&gt;agent-cli&lt;/code&gt; for those without local hardware (someone I know wanted to try it), I consistently find that local execution is faster than waiting on a cloud API.&#10;For example, a local transcription with Whisper + Ollama lands in my clipboard in well under a second—often beating the network round-trip to a remote service.&lt;/p&gt;&#10;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;p&gt;The toolkit includes:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;autocorrect&lt;/code&gt;: Fixes grammar and spelling.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;transcribe&lt;/code&gt;: Uses a local Whisper model for speech-to-text.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;speak&lt;/code&gt;: Converts text to speech with a local TTS engine.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;voice-edit&lt;/code&gt;: A voice-powered clipboard assistant.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;assistant&lt;/code&gt;: A hands-free voice assistant using a wake word.&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;chat&lt;/code&gt;: A conversational AI with tool-calling capabilities.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;It&amp;rsquo;s been a fantastic success in my personal workflow, and it&amp;rsquo;s all open-source for you to try.&#10;Contributions and ideas welcome!&lt;/p&gt;&#10;&lt;p&gt;Once I trusted my toolchain, the obvious next step was to feed &lt;em&gt;all&lt;/em&gt; my data into it.&lt;/p&gt;&#10;&lt;h2 id="4-the-grand-ambition-aibrain-"&gt;4. The Grand Ambition: &lt;code&gt;AIBrain&lt;/code&gt; 🧠 &lt;a class="anchor" href="#4-the-grand-ambition-aibrain-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My second package, &lt;a href="https://github.com/basnijholt/aibrain" target="_blank" rel="noopener"&gt;&lt;code&gt;AIBrain&lt;/code&gt;&lt;/a&gt;, is a much larger endeavor.&#10;The vision is to create a &amp;ldquo;life-OS&amp;rdquo;—a private, local AI that processes all my personal data (emails, calendar, files, photos, messages, browser history, location data) to generate summaries and answer questions, eventually allowing for queries like:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aibrain ask &lt;span class="s2"&gt;&amp;#34;When did I get into landscape photography?&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aibrain ask &lt;span class="s2"&gt;&amp;#34;How many countries did I visit between 2015-2025?&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This project led me to explore the landscape of agentic frameworks.&#10;I started with &lt;strong&gt;CrewAI&lt;/strong&gt;, but I quickly found that despite the hype and funding, it had some painful problems (specifically &lt;a href="https://github.com/crewAIInc/crewAI/issues/3031" target="_blank" rel="noopener"&gt;this issue&lt;/a&gt; where things just fail without any helpful error messages).&#10;I decided to switch to &lt;strong&gt;LangGraph&lt;/strong&gt;, which felt more robust.&#10;I also chose to not use &lt;strong&gt;PydanticAI&lt;/strong&gt; (which I used in &lt;code&gt;agent-cli&lt;/code&gt;) because its &lt;strong&gt;Ollama&lt;/strong&gt; support is just a wrapper around the OpenAI-compatible API, which doesn&amp;rsquo;t expose all the useful options (I even opened &lt;a href="https://github.com/ollama/ollama/pull/11249/" target="_blank" rel="noopener"&gt;a PR&lt;/a&gt; in Ollama to make its OpenAI compatible API more feature-complete, but it has not been merged yet).&lt;/p&gt;&#10;&lt;p&gt;The core infrastructure of &lt;code&gt;AIBrain&lt;/code&gt; is ready.&#10;It can index emails, calendar events, and files in various formats (PDF, Excel, PowerPoint, images).&#10;However, I&amp;rsquo;ve hit a significant challenge.&lt;/p&gt;&#10;&lt;aside class="callout callout-warning"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M236.8,188.09,149.35,36.22h0a24.76,24.76,0,0,0-42.7,0L19.2,188.09a23.51,23.51,0,0,0,0,23.72A24.35,24.35,0,0,0,40.55,224h174.9a24.35,24.35,0,0,0,21.33-12.19A23.51,23.51,0,0,0,236.8,188.09ZM222.93,203.8a8.5,8.5,0,0,1-7.48,4.2H40.55a8.5,8.5,0,0,1-7.48-4.2,7.59,7.59,0,0,1,0-7.72L120.52,44.21a8.75,8.75,0,0,1,15,0l87.45,151.87A7.59,7.59,0,0,1,222.93,203.8ZM120,144V104a8,8,0,0,1,16,0v40a8,8,0,0,1-16,0Zm20,36a12,12,0,1,1-12-12A12,12,0,0,1,140,180Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;p&gt;&lt;strong&gt;🔥 The Performance Bottleneck&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Even with a 12-core CPU and 24GB GPU, processing just a few days’ worth of email made the fans scream for half an hour.&#10;Processing hundreds of gigabytes of personal data is a massive undertaking, and it&amp;rsquo;s clear that my current approach isn&amp;rsquo;t scalable enough.&lt;/p&gt;&#10;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="5-conclusion-the-journey-continues"&gt;5. Conclusion: The Journey Continues &lt;a class="anchor" href="#5-conclusion-the-journey-continues" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This past month has been a whirlwind.&#10;I set out to play games and ended up with two open-source AI packages and a much deeper appreciation for the power sitting in a consumer-grade GPU.&#10;It&amp;rsquo;s incredible what you can achieve with local hardware today.&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;agent-cli&lt;/code&gt; is a finished, polished tool that I use daily.&#10;&lt;code&gt;AIBrain&lt;/code&gt; is a much bigger dream, and I&amp;rsquo;m still figuring out how to solve the performance puzzle to make it truly practical.&#10;Next up: smarter chunking with LangGraph, vector-DB swap-outs, and GPU scheduling tricks.&lt;/p&gt;&#10;&lt;p&gt;This experience has reinforced my passion for local-first software and open-source development.&#10;The future of personal, private AI is being built right now, not just in large corporate labs, but in the homes of hobbyists tinkering with their gaming rigs.&lt;/p&gt;&#10;&lt;p&gt;⭐ If this resonates, star the repos &amp;amp; drop issues/questions.&lt;/p&gt;&#10;&lt;p&gt;And who knows, maybe I&amp;rsquo;ll even find the time to play &amp;lsquo;The Last of Us&amp;rsquo;.&lt;/p&gt;&#10;</description></item><item><title>Agent CLI</title><link>https://www.nijho.lt/project/agent-cli/</link><pubDate>Wed, 25 Jun 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/agent-cli/</guid><description/></item><item><title>gh-download</title><link>https://www.nijho.lt/project/gh-download/</link><pubDate>Fri, 30 May 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/gh-download/</guid><description/></item><item><title>Confessions of an asymmetric hypocrite: on Python dependencies</title><link>https://www.nijho.lt/post/dependencies/</link><pubDate>Sun, 27 Apr 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/dependencies/</guid><description>&lt;h2 id="introduction-the-dependency-dilemma-"&gt;Introduction: The Dependency Dilemma 🤔 &lt;a class="anchor" href="#introduction-the-dependency-dilemma-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I just listened to a podcast &amp;ldquo;&lt;a href="https://www.youtube.com/watch?v=5ZyzeeYZgeM" target="_blank" rel="noopener"&gt;Build software that lasts!&lt;/a&gt;&amp;rdquo; with Bert Hubert (a fellow Dutch), who prefers to reduce the number of dependencies to an absolute minimum (also see his related blog posts [&lt;a href="https://berthub.eu/articles/posts/on-long-term-software-development/" target="_blank" rel="noopener"&gt;1&lt;/a&gt;] and [&lt;a href="https://berthub.eu/articles/posts/a-2024-plea-for-lean-software/" target="_blank" rel="noopener"&gt;2&lt;/a&gt;]).&#10;While I agree one shouldn&amp;rsquo;t use &lt;a href="https://www.npmjs.com/package/is-even" target="_blank" rel="noopener"&gt;&lt;code&gt;is-even&lt;/code&gt;&lt;/a&gt; as a dependency (&lt;a href="https://www.npmjs.com/package/is-odd" target="_blank" rel="noopener"&gt;&lt;code&gt;is-odd&lt;/code&gt;&lt;/a&gt; is a better choice 😏), I think there is a lot of value in reducing the number of dependencies but &lt;em&gt;also&lt;/em&gt; in using the right tool for the job and not continuously re-inventing the wheel.&#10;The podcast got me thinking about my own stance on dependencies and how I actively avoid introducing dependencies into my libraries while expecting (hoping 🤞) others to depend on my work.&lt;/p&gt;&#10;&lt;p&gt;Like many software developers, I&amp;rsquo;ve spent countless hours wrestling with dependencies.&#10;Dependency hell is easy to fall into, &lt;em&gt;especially&lt;/em&gt; in dynamic languages like Python.&#10;Those moments where installing or updating one package breaks another, or where a tiny library pulls in hundreds of transitive dependencies, are frustratingly common.&#10;This frustration partly motivated me to create &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;&lt;code&gt;unidep&lt;/code&gt;&lt;/a&gt;, a tool aimed at simplifying dependency management across different Python tools like &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;conda&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;But working on &lt;code&gt;unidep&lt;/code&gt; and my other projects forces me to think about my own relationship with dependencies.&#10;I&amp;rsquo;ve realized I operate with what feels like an &amp;ldquo;asymmetric hypocrisy.&amp;rdquo;&#10;My standards for the libraries I &lt;em&gt;publish&lt;/em&gt; are extremely strict regarding which external dependencies I &lt;em&gt;consume&lt;/em&gt;.&#10;Yet, as an author of over 30 Python packages, I inherently &lt;em&gt;produce&lt;/em&gt; dependencies, hoping others will adopt my work.&#10;And then, when building applications or CLIs, my caution often disappears entirely.&lt;/p&gt;&#10;&lt;p&gt;This post dives into that asymmetry.&#10;It&amp;rsquo;s about the different hats I wear: the minimalist library author (as a dependency &lt;em&gt;consumer&lt;/em&gt;), the hopeful library author (as a dependency &lt;em&gt;producer&lt;/em&gt;), and the pragmatic application developer.&#10;In this post, I&amp;rsquo;ll explain why I take these different approaches and why it&amp;rsquo;s really about the situation, not a contradiction.&lt;/p&gt;&#10;&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#introduction-the-dependency-dilemma-"&gt;Introduction: The Dependency Dilemma 🤔&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#1-the-library-author-hat--as-dependency-consumer-maximum-caution"&gt;1. The Library Author Hat 🎩 (as Dependency Consumer): Maximum Caution&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-the-library-author-hat--as-dependency-producer-the-paradox"&gt;2. The Library Author Hat 🎩 (as Dependency Producer): The Paradox&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-the-application-developer--contributor-hat--practicality-is-key"&gt;3. The Application Developer / Contributor Hat 🧢: Practicality is Key&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-conclusion-not-hypocrisy-but-context-driven-strategy-"&gt;4. Conclusion: Not Hypocrisy, but Context-Driven Strategy 🙏&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I&amp;rsquo;m extremely strict about adding external dependencies to my &lt;em&gt;libraries&lt;/em&gt;, preferring widely-used, low-dependency packages.&#10;Simultaneously, I &lt;em&gt;create&lt;/em&gt; libraries expecting adoption.&#10;For &lt;em&gt;applications&lt;/em&gt; (especially CLIs in isolated environments), I&amp;rsquo;m far more liberal with dependencies to build features quickly.&#10;It&amp;rsquo;s all about managing risks depending on the situation.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="1-the-library-author-hat--as-dependency-consumer-maximum-caution"&gt;1. The Library Author Hat 🎩 (as Dependency Consumer): Maximum Caution &lt;a class="anchor" href="#1-the-library-author-hat--as-dependency-consumer-maximum-caution" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When I build and publish an open-source Python library – like &lt;a href="https://github.com/python-adaptive/adaptive" target="_blank" rel="noopener"&gt;&lt;code&gt;Adaptive&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt;&lt;/a&gt;, or &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;&lt;code&gt;unidep&lt;/code&gt;&lt;/a&gt; – my primary focus shifts to the &lt;em&gt;users&lt;/em&gt; who will depend on my code.&#10;My choices directly impact their projects.&#10;This responsibility means I have to be very careful about adding external dependencies:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Minimize and Scrutinize:&lt;/strong&gt; Fewer dependencies mean fewer potential conflicts for users.&#10;I aggressively minimize the core runtime dependency list.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Favor Battle-Tested Dependencies:&lt;/strong&gt; I strongly prefer dependencies that are already widely adopted and trusted by the community.&#10;(I have been called that person that always wants the latest and greatest, the truth is that I am a sucker for shiny new things but typically only adopt them once they are already quite popular.)&#10;Think &lt;code&gt;numpy&lt;/code&gt; or &lt;code&gt;cloudpickle&lt;/code&gt;.&#10;These are less likely to cause unexpected issues or disappear overnight.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Low Transitive Dependency Count:&lt;/strong&gt; A key criterion is how many &lt;em&gt;other&lt;/em&gt; packages a potential dependency pulls in.&#10;Ideally, I choose dependencies with zero or very few further dependencies.&#10;For example, in &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt;&lt;/a&gt;, dependencies like &lt;a href="https://numpy.org/" target="_blank" rel="noopener"&gt;&lt;code&gt;numpy&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://networkx.org/" target="_blank" rel="noopener"&gt;&lt;code&gt;networkx&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://github.com/cloudpipe/cloudpickle" target="_blank" rel="noopener"&gt;&lt;code&gt;cloudpickle&lt;/code&gt;&lt;/a&gt; are good examples – they are popular and don&amp;rsquo;t rely on many other things.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Optional Dependencies for Heavy Features:&lt;/strong&gt; If a feature really needs a large or less common dependency, I try making it a &lt;em&gt;optional&lt;/em&gt; extra (&lt;code&gt;project.optional-dependencies&lt;/code&gt;).&#10;Users opt-in only if they need that specific capability.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Avoid Pinning (Mostly):&lt;/strong&gt; As mentioned before, hard version pins (&lt;code&gt;==1.2.3&lt;/code&gt;) in libraries are usually harmful.&#10;Minimum versions (&lt;code&gt;&amp;gt;=1.2&lt;/code&gt;) or safe upper bounds (&lt;code&gt;&amp;lt;2.0&lt;/code&gt;) are preferred.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;License Awareness:&lt;/strong&gt; The license of a dependency matters immensely, especially regarding compatibility and obligations (&lt;a href="https://www.nijho.lt/post/gpl/"&gt;GPL considerations&lt;/a&gt;).&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The guiding principle is: &lt;strong&gt;Do not cause pain for your users.&lt;/strong&gt;&#10;A library should integrate smoothly into diverse environments.&lt;/p&gt;&#10;&lt;h2 id="2-the-library-author-hat--as-dependency-producer-the-paradox"&gt;2. The Library Author Hat 🎩 (as Dependency Producer): The Paradox &lt;a class="anchor" href="#2-the-library-author-hat--as-dependency-producer-the-paradox" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s where the &amp;ldquo;hypocrisy&amp;rdquo; might seem to surface.&#10;While I&amp;rsquo;m cautious about &lt;em&gt;consuming&lt;/em&gt; dependencies for my libraries, I am also the author of over 30 packages.&#10;I invest time and effort into these tools, naturally hoping they prove useful and that others &lt;em&gt;will adopt them&lt;/em&gt; – making &lt;em&gt;my&lt;/em&gt; libraries dependencies in &lt;em&gt;their&lt;/em&gt; projects.&lt;/p&gt;&#10;&lt;p&gt;Is it hypocritical to be wary of adding dependencies while simultaneously creating them for others?&#10;Perhaps slightly, but it&amp;rsquo;s also the nature of contributing to an ecosystem.&#10;I try to handle this by strictly following the rules from Section 1 for my own libraries.&#10;If I expect others to rely on my code, I feel responsible to make that dependency as&#10;low-risk and stable as possible.&#10;I want my libraries to be the kind of well-behaved, minimal-friction dependencies&#10;that I myself would be comfortable adding.&lt;/p&gt;&#10;&lt;h2 id="3-the-application-developer--contributor-hat--practicality-is-key"&gt;3. The Application Developer / Contributor Hat 🧢: Practicality is Key &lt;a class="anchor" href="#3-the-application-developer--contributor-hat--practicality-is-key" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When the context shifts to building an application – like a command-line tool, a web service, or a personal project – my rules become &lt;em&gt;much&lt;/em&gt; less strict.&#10;The key differences are isolation (keeping things separate) and how far the effects reach.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Isolation Reduces Risk:&lt;/strong&gt; Applications often run in controlled, isolated environments.&#10;&lt;a href="https://www.docker.com/" target="_blank" rel="noopener"&gt;&lt;code&gt;Docker&lt;/code&gt;&lt;/a&gt;, &lt;code&gt;conda&lt;/code&gt;/&lt;a href="https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html" target="_blank" rel="noopener"&gt;&lt;code&gt;micromamba&lt;/code&gt;&lt;/a&gt; (&lt;a href="https://www.nijho.lt/post/python-environments/"&gt;Python Environment Jungle post&lt;/a&gt;), or simple &lt;code&gt;venv&lt;/code&gt;s contain the dependencies.&#10;A conflict within the application&amp;rsquo;s environment doesn&amp;rsquo;t break the user&amp;rsquo;s entire system or other unrelated projects.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;CLIs as a Prime Example:&lt;/strong&gt; Many tools I&amp;rsquo;ve built are CLIs (like &lt;a href="https://github.com/basnijholt/tuitorial" target="_blank" rel="noopener"&gt;&lt;code&gt;tuitorial&lt;/code&gt;&lt;/a&gt; or helpers within my &lt;a href="https://github.com/basnijholt/dotfiles" target="_blank" rel="noopener"&gt;&lt;code&gt;dotfiles&lt;/code&gt;&lt;/a&gt;).&#10;These might have tens of dependencies.&#10;Because they are installed into isolated environments (often via &lt;a href="https://docs.astral.sh/uv/" target="_blank" rel="noopener"&gt;&lt;code&gt;uv&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://pipx.pypa.io/stable/" target="_blank" rel="noopener"&gt;&lt;code&gt;pipx&lt;/code&gt;&lt;/a&gt;, or within a dedicated &lt;code&gt;conda&lt;/code&gt; env), this complexity is okay.&#10;The risk is contained.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Developer Tools Welcome:&lt;/strong&gt; Productivity boosters like &lt;a href="https://beta.ruff.rs/docs/rules/" target="_blank" rel="noopener"&gt;&lt;code&gt;ruff&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://mypy.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;&lt;code&gt;mypy&lt;/code&gt;&lt;/a&gt;, etc., are fair game as development dependencies (&lt;a href="https://www.nijho.lt/post/best-python-dev-tooling/"&gt;My Python Project Toolkit&lt;/a&gt;).&#10;They don&amp;rsquo;t impact the end-user runtime (if packaged properly), so I am much more lenient with dev tools.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Focus on Functionality:&lt;/strong&gt; The goal is often to deliver features or solve a problem efficiently.&#10;Leveraging existing libraries, even those with dependencies, is usually faster and more robust than reimplementing complex logic.&#10;It usually makes more sense to use existing libraries.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;In this context, the focus changes from &lt;em&gt;minimizing external impact&lt;/em&gt; (libraries) to &lt;em&gt;maximizing internal capability and maintainability&lt;/em&gt; within a controlled scope (applications).&lt;/p&gt;&#10;&lt;h2 id="4-conclusion-not-hypocrisy-but-context-driven-strategy-"&gt;4. Conclusion: Not Hypocrisy, but Context-Driven Strategy 🙏 &lt;a class="anchor" href="#4-conclusion-not-hypocrisy-but-context-driven-strategy-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My approach to dependencies &lt;em&gt;is&lt;/em&gt; &amp;ldquo;it depends on the situation&amp;rdquo;.&#10;I&amp;rsquo;m stricter about dependencies for my libraries than for my applications or personal tools.&#10;I am cautious about adopting dependencies while actively producing them for others.&lt;/p&gt;&#10;&lt;p&gt;But I don&amp;rsquo;t see this as hypocrisy.&#10;It&amp;rsquo;s a practical approach based on different situations and goals:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Libraries (as Consumer):&lt;/strong&gt; Focus on keeping the software community stable and making things easy for users. Be conservative.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Libraries (as Producer):&lt;/strong&gt; Create software responsibly, following the same high standards you expect from others.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Applications:&lt;/strong&gt; Focus on adding features and developing quickly within separate environments. Be pragmatic.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Understanding the different risks and goals for each situation helps make good decisions about dependencies.&#10;It&amp;rsquo;s about choosing the right trade-offs for the specific situation, whether you&amp;rsquo;re building a foundational block for the community or a specific tool for a controlled environment.&lt;/p&gt;&#10;&lt;p&gt;How do you handle dependencies across different types of projects?&#10;Do you find yourself navigating similar questions?&#10;Share your thoughts!&lt;/p&gt;&#10;</description></item><item><title>Functions over classes? Why I prefer a simpler, functional style in Python 🐍</title><link>https://www.nijho.lt/post/functional-python/</link><pubDate>Mon, 14 Apr 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/functional-python/</guid><description>&lt;p&gt;Over years of building software, from scientific simulation tools like &lt;a href="https://github.com/python-adaptive/adaptive" target="_blank" rel="noopener"&gt;&lt;code&gt;Adaptive&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt;&lt;/a&gt; to package management packages like &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;&lt;code&gt;unidep&lt;/code&gt;&lt;/a&gt;, I&amp;rsquo;ve found myself preferring a certain coding style: using small, focused functions more than large classes with complex internal state and deep inheritance hierarchies.&#10;This isn&amp;rsquo;t about dogma, but about a pragmatic approach that, in my experience, leads to code that is easier to understand, test, and maintain.&lt;/p&gt;&#10;&lt;p&gt;This post outlines why I lean towards this functional-inspired style in my Python projects, while still recognizing where classes are the right tool.&lt;/p&gt;&#10;&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-taming-complexity-avoiding-the-over-engineering-trap-"&gt;1. Taming Complexity: Avoiding the Over-Engineering Trap 🧠&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-the-power-of-small-focused-functions-"&gt;2. The Power of Small, Focused Functions 🎯&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-keeping-classes-simple--using-them-where-they-fit-"&gt;3. Keeping Classes Simple &amp;amp; Using Them Where They Fit 🛠️&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-the-testability-advantage-"&gt;4. The Testability Advantage ✅&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-its-about-pragmatism-not-dogma-"&gt;5. It&amp;rsquo;s About Pragmatism, Not Dogma 🙏&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-taming-complexity-avoiding-the-over-engineering-trap-"&gt;1. Taming Complexity: Avoiding the Over-Engineering Trap 🧠 &lt;a class="anchor" href="#1-taming-complexity-avoiding-the-over-engineering-trap-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;One of the biggest challenges in software development is managing complexity.&#10;As systems grow, keeping track of how different parts interact and understanding the internal state of objects can become a lot to keep track of mentally (high cognitive load).&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;ve often observed a pattern, especially when developers first start using object-oriented programming: the tendency to use classes &lt;em&gt;everywhere&lt;/em&gt;.&#10;This can lead to over-engineering – introducing many layers of inheritance, abstract base classes with only a single implementation, and generally creating complexity when simplicity would suffice.&#10;Thinking you&amp;rsquo;re being clever by building complex class structures can sometimes just be a form of overgeneralization that makes the code harder, not easier, to follow.&lt;/p&gt;&#10;&lt;p&gt;Classes, particularly those with many methods modifying internal attributes (&lt;code&gt;self.x&lt;/code&gt;, &lt;code&gt;self.y&lt;/code&gt;, etc.), require you to constantly hold their state in your head.&#10;When you call a method &lt;code&gt;object.do_something()&lt;/code&gt;, the outcome might depend not just on the arguments you pass, but also on the object&amp;rsquo;s history – what methods were called before, and in what order?&#10;This hidden state can make reasoning about the code&amp;rsquo;s behavior difficult and debugging tricky.&lt;/p&gt;&#10;&lt;p&gt;Functions, particularly when kept small and focused, tend to be more explicit.&#10;Ideally, a function takes inputs and produces outputs, with minimal side effects (or ideally, none - making them pure functions).&#10;Its behavior is primarily determined by its arguments.&#10;This makes the code flow easier to follow: data comes in, transformation happens, data goes out.&#10;There&amp;rsquo;s less hidden context to juggle mentally.&lt;/p&gt;&#10;&lt;h2 id="2-the-power-of-small-focused-functions-"&gt;2. The Power of Small, Focused Functions 🎯 &lt;a class="anchor" href="#2-the-power-of-small-focused-functions-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This leads directly to the preference for small functions, each doing one thing well (adhering to the Single Responsibility Principle).&#10;When functions are concise and have a clear purpose:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Readability Improves:&lt;/strong&gt; It&amp;rsquo;s easier to understand what a small, well-named function does quickly.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Reusability Increases:&lt;/strong&gt; Small, focused functions are more likely to be reusable in different parts of the codebase or even in other projects.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Refactoring is Simpler:&lt;/strong&gt; Modifying or replacing a small unit of logic is less risky than altering a large, complex method within a class.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This philosophy is something I actively apply.&#10;Looking at the codebase for &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;&lt;code&gt;unidep&lt;/code&gt;&lt;/a&gt;, for example, you&amp;rsquo;ll find many utility functions in modules like &lt;code&gt;_dependencies_parsing.py&lt;/code&gt; or &lt;code&gt;_conda_env.py&lt;/code&gt;, each handling a specific, well-defined task.&lt;/p&gt;&#10;&lt;h2 id="3-keeping-classes-simple--using-them-where-they-fit-"&gt;3. Keeping Classes Simple &amp;amp; Using Them Where They Fit 🛠️ &lt;a class="anchor" href="#3-keeping-classes-simple--using-them-where-they-fit-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;So, where do classes fit in this picture?&lt;/p&gt;&#10;&lt;p&gt;Firstly, as &lt;strong&gt;simple data structures&lt;/strong&gt; – think &lt;code&gt;NamedTuple&lt;/code&gt;s or &lt;code&gt;dataclasses&lt;/code&gt; (preferably with &lt;code&gt;frozen=True&lt;/code&gt;).&#10;Their main role is to group related data attributes together, providing a clear structure for passing information around.&#10;They hold data, but don&amp;rsquo;t typically manage complex state transitions.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Example of a simple data class from unidep&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Use frozen=True for immutability&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;which&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;conda&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pip&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ... other simple attributes ...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Used to pass structured data, not to manage complex state&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_spec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Spec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ... function logic using spec data ...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;pass&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Secondly, and importantly, classes are excellent for &lt;strong&gt;defining user-facing APIs&lt;/strong&gt;.&#10;For the end-user of a library, having a single object with easily discoverable methods (&lt;code&gt;object.&amp;lt;tab&amp;gt;&lt;/code&gt;) is often a great user experience.&lt;/p&gt;&#10;&lt;p&gt;A good example from my own work is the &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt;&lt;/a&gt; package.&#10;It provides two main classes, &lt;code&gt;Pipeline&lt;/code&gt; and &lt;code&gt;PipeFunc&lt;/code&gt;, which encapsulate the core functionality.&#10;Users interact primarily with these objects and their methods.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Simplified example from pipefunc&amp;#39;s user perspective&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pipefunc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Pipeline&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;some_function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;output_node&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, &lt;strong&gt;inside&lt;/strong&gt; the methods of these API classes (&lt;code&gt;Pipeline.run&lt;/code&gt;, &lt;code&gt;Pipeline.add&lt;/code&gt;, etc.), the actual logic is often delegated to smaller, internal functions.&#10;The class acts as a clean interface or facade, orchestrating calls to these more focused, functional units.&#10;This gives the user a nice API while keeping the internal implementation modular and testable.&lt;/p&gt;&#10;&lt;p&gt;I also actively avoid deep inheritance hierarchies.&#10;Composition (having objects contain other objects) often provides a more flexible and explicit way to combine functionality without the tight coupling and complexity inheritance can introduce.&lt;/p&gt;&#10;&lt;h2 id="4-the-testability-advantage-"&gt;4. The Testability Advantage ✅ &lt;a class="anchor" href="#4-the-testability-advantage-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;One of the most significant practical benefits of prioritizing functions and simple classes is testability.&#10;Small, pure (or mostly pure) functions are incredibly easy to test:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Provide known inputs.&lt;/li&gt;&#10;&lt;li&gt;Assert expected outputs.&lt;/li&gt;&#10;&lt;li&gt;Minimal setup or mocking is required because there&amp;rsquo;s no complex internal state to manage.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Even when testing the methods of the API classes (like &lt;code&gt;Pipeline&lt;/code&gt; in &lt;code&gt;pipefunc&lt;/code&gt;), the fact that they often delegate to well-tested internal functions simplifies the process.&#10;You focus on testing the orchestration and interaction logic at the class level, knowing the underlying functional units are already verified.&lt;/p&gt;&#10;&lt;p&gt;Testing classes with intricate internal state inherently involves more setup (instantiating the object, getting it into the right state before calling the method under test) and potentially more complex assertions to verify state changes.&#10;Aiming for more functional units significantly simplifies the testing process.&lt;/p&gt;&#10;&lt;h2 id="5-its-about-pragmatism-not-dogma-"&gt;5. It&amp;rsquo;s About Pragmatism, Not Dogma 🙏 &lt;a class="anchor" href="#5-its-about-pragmatism-not-dogma-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This isn&amp;rsquo;t a wholesale rejection of object-oriented programming.&#10;It&amp;rsquo;s about choosing the right tool for the job and prioritizing clarity, testability, and maintainability.&#10;Classes are fantastic tools, especially for building clear APIs and structuring data.&#10;Deep inheritance and complex state management, however, should be used carefully, not just because the language allows it.&lt;/p&gt;&#10;&lt;p&gt;For much of the work I do – building libraries, tools, and performing data analysis or simulations – I&amp;rsquo;ve found that prioritizing functions, keeping API classes focused, minimizing internal state, and avoiding deep inheritance leads to code that &lt;em&gt;I&lt;/em&gt; find easier to write, understand, debug, and maintain in the long run.&#10;The reduction in cognitive load and the ease of testing are strong points for how I work.&lt;/p&gt;&#10;&lt;p&gt;If you often find yourself struggling with complex state management or tangled inheritance trees, perhaps consider if a simpler, more functional internal structure, potentially fronted by a clean class-based API, might bring some clarity and simplicity to your projects too.&lt;/p&gt;&#10;</description></item><item><title>Vibe coding worked—until the 15‑hour cleanup</title><link>https://www.nijho.lt/post/vibe-coding/</link><pubDate>Fri, 21 Mar 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/vibe-coding/</guid><description>&lt;h2 id="i-tried-vibe-coding-for-3-hours-my-application-worked-but-then-i-spent-15-hours-fixing-it"&gt;I tried &amp;ldquo;vibe coding&amp;rdquo; for 3 hours, my application worked, but then I spent 15 hours fixing it &lt;a class="anchor" href="#i-tried-vibe-coding-for-3-hours-my-application-worked-but-then-i-spent-15-hours-fixing-it" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;That&amp;rsquo;s not an exaggeration—it&amp;rsquo;s really what happened.&lt;/p&gt;&#10;&lt;p&gt;Recently, I tried &amp;ldquo;&lt;a href="https://en.wikipedia.org/wiki/Vibe_coding" target="_blank" rel="noopener"&gt;Vibe coding&lt;/a&gt;,&amp;rdquo; a term coined by Andrej Karpathy, a prominent AI researcher, co-founder of OpenAI, and former AI lead at Tesla.&#10;In &lt;a href="https://x.com/karpathy/status/1886192184808149383" target="_blank" rel="noopener"&gt;his viral tweet&lt;/a&gt; from February 2025, Karpathy described vibe coding as a programming approach where you &amp;ldquo;fully give in to the vibes, embrace exponentials, and forget that the code even exists.&amp;rdquo;&#10;It&amp;rsquo;s essentially about letting AI handle the coding based on natural language prompts without closely reviewing the generated code.&lt;/p&gt;&#10;&lt;p&gt;Intrigued by the hype, I decided to experiment with vibe coding using &lt;a href="https://www.cursor.com/" target="_blank" rel="noopener"&gt;Cursor&lt;/a&gt; on a new Python project I called &lt;a href="https://github.com/basnijholt/dotbins" target="_blank" rel="noopener"&gt;dotbins&lt;/a&gt; (&lt;a href="../dotbins"&gt;check out my detailed write-up here&lt;/a&gt;).&amp;quot;&#10;&lt;a href="https://github.com/basnijholt/dotbins" target="_blank" rel="noopener"&gt;Dotbins&lt;/a&gt; manages CLI tool binaries within Git repositories, automatically downloading the correct binary from GitHub for the user&amp;rsquo;s platform and architecture, significantly simplifying my dotfiles and bootstrapping process.&#10;I am super excited about this project but it is not what this post is about.&lt;/p&gt;&#10;&lt;p&gt;Initially, it was fun to get something working so quickly.&#10;Within just three hours, I had a functioning prototype—without carefully examining the code.&#10;But I had unknowingly created a monster—a true &lt;a href="https://en.wikipedia.org/wiki/Spaghetti_code" target="_blank" rel="noopener"&gt;spaghetti&lt;/a&gt; monster! 🍝&lt;/p&gt;&#10;&lt;p&gt;Once I started reviewing the AI-generated code, I realized it was totally incomprehensible.&#10;The AI had implemented the most complicated and roundabout ways to achieve simple tasks.&#10;It introduced redundant checks for impossible conditions, irrelevant error handling, and execution paths that would never trigger.&#10;The code worked superficially, but beneath the surface, it was chaotic and nearly impossible to understand or maintain.&lt;/p&gt;&#10;&lt;p&gt;This realization turned three hours of fun into fifteen hours of meticulous debugging, refactoring, and cleaning up the mess.&#10;Essentially, I had to rewrite the entire project.&lt;/p&gt;&#10;&lt;p&gt;My main takeaway: AI-assisted coding, or &amp;ldquo;vibe coding,&amp;rdquo; is incredibly powerful for quick prototyping but dangerous when used without oversight.&#10;According to &lt;a href="https://www.youtube.com/watch?v=IACHfKmZMr8" target="_blank" rel="noopener"&gt;this recent Y Combinator discussion&lt;/a&gt;, many startup founders are building products almost entirely with AI-generated code, &amp;ldquo;vibing&amp;rdquo; the code, meaning they let the AI write the code and don&amp;rsquo;t review it.&#10;For writing a couple of hundred lines of code it likely works pretty well, but for anything more complex, it&amp;rsquo;s a recipe for disaster.&#10;Personally, I would never invest in, build upon, or use such products—they&amp;rsquo;re essentially constructing technical prisons for themselves.&lt;/p&gt;&#10;&lt;p&gt;AI is fantastic for inspiration, quick prototyping, writing code, and refactoring—but letting it run wild without careful human review can quickly lead to severe technical debt and hidden bugs.&lt;/p&gt;&#10;&lt;p&gt;Thanks for reading!&lt;/p&gt;&#10;</description></item><item><title>dotbins: managing binary tools in your dotfiles 🧰</title><link>https://www.nijho.lt/post/dotbins/</link><pubDate>Tue, 11 Mar 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/dotbins/</guid><description>&lt;p&gt;In my &lt;a href="../terminal-ninja/"&gt;previous post about terminal productivity&lt;/a&gt;, I shared a setup combining tools like &lt;code&gt;zsh&lt;/code&gt;, &lt;code&gt;oh-my-zsh&lt;/code&gt;, &lt;code&gt;zsh-z&lt;/code&gt;, and various plugins to create a highly efficient command-line environment. While that setup served me well for years, I&amp;rsquo;ve recently encountered a challenge: incorporating modern, compiled CLI tools across all my environments without administrative privileges.&lt;/p&gt;&#10;&lt;p&gt;Today, I&amp;rsquo;m excited to &lt;a href="https://github.com/basnijholt/dotbins/" target="_blank" rel="noopener"&gt;introduce &lt;strong&gt;&lt;code&gt;dotbins&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; - a solution I created to solve this exact problem.&lt;/p&gt;&#10;&lt;h2 id="the-modern-cli-tools-problem"&gt;The Modern CLI Tools Problem &lt;a class="anchor" href="#the-modern-cli-tools-problem" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Over the past few years, we&amp;rsquo;ve seen an explosion of incredible, modern CLI tools written in languages like Rust and Go.&#10;Tools like:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/ajeetdsouza/zoxide" target="_blank" rel="noopener"&gt;&lt;code&gt;zoxide&lt;/code&gt;&lt;/a&gt; (a smarter, faster alternative to &lt;code&gt;zsh-z&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/sharkdp/bat" target="_blank" rel="noopener"&gt;&lt;code&gt;bat&lt;/code&gt;&lt;/a&gt; (a &lt;code&gt;cat&lt;/code&gt; replacement with syntax highlighting)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/eza-community/eza" target="_blank" rel="noopener"&gt;&lt;code&gt;eza&lt;/code&gt;&lt;/a&gt; (a modern &lt;code&gt;ls&lt;/code&gt; alternative)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/dandavison/delta" target="_blank" rel="noopener"&gt;&lt;code&gt;delta&lt;/code&gt;&lt;/a&gt; (a better diff viewer for Git)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/junegunn/fzf" target="_blank" rel="noopener"&gt;&lt;code&gt;fzf&lt;/code&gt;&lt;/a&gt; (a fuzzy finder for your terminal)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/BurntSushi/ripgrep" target="_blank" rel="noopener"&gt;&lt;code&gt;ripgrep&lt;/code&gt;&lt;/a&gt; (a faster, better &lt;code&gt;grep&lt;/code&gt;)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/atuinsh/atuin" target="_blank" rel="noopener"&gt;&lt;code&gt;atuin&lt;/code&gt;&lt;/a&gt; (a shell history with search and sync)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;These tools significantly enhance terminal productivity, but they come with a catch: &lt;strong&gt;they&amp;rsquo;re compiled binaries&lt;/strong&gt;, not simple scripts.&#10;While my previous setup worked well because all the tools were basic shell scripts I could include as submodules in my dotfiles repository, these modern alternatives require installation.&lt;/p&gt;&#10;&lt;p&gt;This became frustrating when working on:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Remote systems where I lacked admin/sudo permissions&lt;/li&gt;&#10;&lt;li&gt;Machines that I use only temporarily&lt;/li&gt;&#10;&lt;li&gt;Systems where I didn&amp;rsquo;t want to install package managers&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;I&amp;rsquo;d spend time carefully setting up my dotfiles with all my configurations, only to find that the actual tools they relied on weren&amp;rsquo;t available.&lt;/p&gt;&#10;&lt;h2 id="enter-dotbins-"&gt;Enter &lt;code&gt;dotbins&lt;/code&gt; 🧰 &lt;a class="anchor" href="#enter-dotbins-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/basnijholt/dotbins/" target="_blank" rel="noopener"&gt;&lt;code&gt;dotbins&lt;/code&gt;&lt;/a&gt; was born out of this frustration.&#10;It&amp;rsquo;s a utility that manages pre-compiled binaries for CLI tools across multiple platforms and architectures, right in your dotfiles repository.&lt;/p&gt;&#10;&lt;p&gt;The key insight: &lt;strong&gt;track pre-compiled binaries in a separate Git repository&lt;/strong&gt; (using Git LFS for efficiency), include this repository as a submodule in your dotfiles, and ensure all your essential tools are immediately available on any system.&lt;/p&gt;&#10;&lt;p&gt;No package manager, no sudo, no problem.&lt;/p&gt;&#10;&lt;h2 id="how-dotbins-works"&gt;How &lt;code&gt;dotbins&lt;/code&gt; Works &lt;a class="anchor" href="#how-dotbins-works" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;dotbins&lt;/code&gt; handles several critical tasks:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Downloads&lt;/strong&gt; binaries directly from GitHub releases for your selected tools&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Organizes&lt;/strong&gt; them by platform (macOS, Linux) and architecture (amd64, arm64)&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Tracks&lt;/strong&gt; installed versions and update timestamps&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Extracts&lt;/strong&gt; binaries from archives in various formats&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Integrates&lt;/strong&gt; seamlessly with your shell via a simple PATH addition&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;The configuration is straightforward - a YAML file where you define which tools you want and how to get them:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ripgrep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;BurntSushi/ripgrep&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;extract_binary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;binary_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;rg&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;binary_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ripgrep-*/rg&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;asset_patterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;linux&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ripgrep-{version}-{arch}-unknown-linux-musl.tar.gz&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;macos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ripgrep-{version}-{arch}-apple-darwin.tar.gz&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;arch_map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;amd64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;x86_64&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;arm64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;aarch64&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="my-workflow-a-two-repository-approach"&gt;My Workflow: A Two-Repository Approach &lt;a class="anchor" href="#my-workflow-a-two-repository-approach" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My approach uses two repositories:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;strong&gt;Private dotfiles repository&lt;/strong&gt; containing all my configurations&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Public &lt;a href="https://github.com/basnijholt/mydotbins" target="_blank" rel="noopener"&gt;mydotbins repository&lt;/a&gt;&lt;/strong&gt; containing all the binaries&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;I include &lt;code&gt;mydotbins&lt;/code&gt; as a submodule in my dotfiles repository.&#10;This separation keeps large binary files out of my main dotfiles repository and allows me to share the binary collection publicly while keeping my personal configurations private.&lt;/p&gt;&#10;&lt;p&gt;When I clone my dotfiles on a new system, I get not just my configuration files, but also all the CLI tools I depend on for productivity.&lt;/p&gt;&#10;&lt;h2 id="modern-alternatives-to-my-previous-setup"&gt;Modern Alternatives to My Previous Setup &lt;a class="anchor" href="#modern-alternatives-to-my-previous-setup" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;With &lt;code&gt;dotbins&lt;/code&gt;, I&amp;rsquo;ve been able to upgrade several tools from my previous setup:&lt;/p&gt;&#10;&lt;div class="table-wrap"&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;Old Tool&lt;/th&gt;&#10;&lt;th&gt;Modern Alternative&lt;/th&gt;&#10;&lt;th&gt;Advantage&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/agkozak/zsh-z" target="_blank" rel="noopener"&gt;&lt;code&gt;zsh-z&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/ajeetdsouza/zoxide" target="_blank" rel="noopener"&gt;&lt;code&gt;zoxide&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Faster, smarter, works across shells&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;cat&lt;/code&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/sharkdp/bat" target="_blank" rel="noopener"&gt;&lt;code&gt;bat&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Syntax highlighting, Git integration&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;ls&lt;/code&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/eza-community/eza" target="_blank" rel="noopener"&gt;&lt;code&gt;eza&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Better formatting, Git integration&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;code&gt;grep&lt;/code&gt;&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/BurntSushi/ripgrep" target="_blank" rel="noopener"&gt;&lt;code&gt;ripgrep&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Much faster, respects &lt;code&gt;.gitignore&lt;/code&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Control + R&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/atuinsh/atuin" target="_blank" rel="noopener"&gt;&lt;code&gt;atuin&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Shell history with search and sync&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;Default Git diff&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/dandavison/delta" target="_blank" rel="noopener"&gt;&lt;code&gt;delta&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Syntax highlighting, better visual diffs&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;N/A&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/junegunn/fzf" target="_blank" rel="noopener"&gt;&lt;code&gt;fzf&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Fuzzy finding makes everything better&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;N/A&lt;/td&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/atuinsh/atuin" target="_blank" rel="noopener"&gt;&lt;code&gt;atuin&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Shell history with search and sync&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;The best part? All of these are now automatically available on every machine where I clone my dotfiles, without any additional installation steps.&lt;/p&gt;&#10;&lt;h2 id="getting-started-with-dotbins"&gt;Getting Started with &lt;code&gt;dotbins&lt;/code&gt; &lt;a class="anchor" href="#getting-started-with-dotbins" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;If you&amp;rsquo;d like to adopt this approach, here&amp;rsquo;s how to get started:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Install &lt;code&gt;dotbins&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pip install dotbins&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Or better yet, use uv:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uvx dotbins&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Create a configuration file at &lt;code&gt;~/.config/dotbins/config.yaml&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tools_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;~/.mydotbins/tools&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;platforms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;linux&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;amd64&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;arm64&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;macos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;arm64&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Add your favorite tools here&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;fzf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;junegunn/fzf&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;extract_binary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;binary_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;fzf&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;binary_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;fzf&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;asset_patterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;fzf-{version}-{platform}_{arch}.tar.gz&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;platform_map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;macos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;darwin&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Initialize and download your tools:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dotbins init&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dotbins update&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Add the shell integration to your &lt;code&gt;.zshrc&lt;/code&gt; or &lt;code&gt;.bashrc&lt;/code&gt;:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# dotbins - Add platform-specific binaries to PATH&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;_os&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;uname -s &lt;span class="p"&gt;|&lt;/span&gt; tr &lt;span class="s1"&gt;&amp;#39;[:upper:]&amp;#39;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;[:lower:]&amp;#39;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$_os&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;darwin&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;_os&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;macos&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;_arch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;uname -m&lt;span class="k"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$_arch&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;x86_64&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;_arch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;amd64&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$_arch&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;aarch64&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$_arch&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;arm64&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;_arch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;arm64&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.mydotbins/tools/&lt;/span&gt;&lt;span class="nv"&gt;$_os&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$_arch&lt;/span&gt;&lt;span class="s2"&gt;/bin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;(Optional) Track your binaries in a separate Git repository with Git LFS for efficiency&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="finding-new-tools-to-add"&gt;Finding New Tools to Add &lt;a class="anchor" href="#finding-new-tools-to-add" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Not sure which pattern to use for a new tool? The &lt;code&gt;analyze&lt;/code&gt; command is here to help:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;dotbins analyze sharkdp/bat&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will suggest a configuration for the tool based on its GitHub release assets.&lt;/p&gt;&#10;&lt;h2 id="conclusion-the-evolution-of-my-terminal-setup"&gt;Conclusion: The Evolution of My Terminal Setup &lt;a class="anchor" href="#conclusion-the-evolution-of-my-terminal-setup" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;With &lt;a href="https://github.com/basnijholt/dotbins/" target="_blank" rel="noopener"&gt;&lt;code&gt;dotbins&lt;/code&gt;&lt;/a&gt;, my terminal productivity setup has evolved beyond what I described in my &lt;a href="../terminal-ninja/"&gt;previous post&lt;/a&gt;.&#10;I can now seamlessly incorporate the best modern CLI tools without worrying about installation or administrative privileges.&lt;/p&gt;&#10;&lt;p&gt;The beauty of this approach is its portability—I get a consistent, powerful environment on any machine where I clone my dotfiles.&lt;/p&gt;&#10;&lt;p&gt;If you&amp;rsquo;re a terminal power user who works across multiple environments, give &lt;a href="https://github.com/basnijholt/dotbins" target="_blank" rel="noopener"&gt;&lt;code&gt;dotbins&lt;/code&gt;&lt;/a&gt; a try.&#10;It might just be the missing piece in your dotfiles setup!&lt;/p&gt;&#10;</description></item><item><title>dotbins</title><link>https://www.nijho.lt/project/dotbins/</link><pubDate>Sun, 09 Mar 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/dotbins/</guid><description/></item><item><title>pixi-to-conda-lock</title><link>https://www.nijho.lt/project/pixi-to-conda-lock/</link><pubDate>Wed, 26 Feb 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/pixi-to-conda-lock/</guid><description/></item><item><title>GPL for Python packaging ⚖️</title><link>https://www.nijho.lt/post/gpl/</link><pubDate>Mon, 10 Feb 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/gpl/</guid><description>&lt;h2 id="understanding-gpl-for-python-packaging"&gt;Understanding GPL for Python Packaging &lt;a class="anchor" href="#understanding-gpl-for-python-packaging" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Over the last 10 years–since I started working on and with open source projects–the GPL topic has frequently come up with friends and colleagues.&#10;I simply write this post as a reminder to myself and to have something concise and clear to link to later.&#10;Also, &lt;a href="https://en.wikipedia.org/wiki/IANAL" target="_blank" rel="noopener"&gt;IANAL&lt;/a&gt;, so if you are relying on this post, please do your own research, I have included all the links I used to make this post.&lt;/p&gt;&#10;&lt;p&gt;The GNU General Public License (GPL) is widely used in the open-source world, but its implications can be unclear, especially regarding Python packaging.&#10;If you develop or distribute Python software, understanding GPL variants is crucial.&lt;/p&gt;&#10;&lt;h3 id="what-is-gpl"&gt;What is GPL? &lt;a class="anchor" href="#what-is-gpl" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;GPL is a strong copyleft license—meaning software using GPL-licensed code must also be released under the GPL.&#10;There are several key variants relevant to Python:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;GPLv2 &amp;amp; GPLv3&lt;/strong&gt;:&#10;These licenses require that software using GPL code (directly or indirectly) also be open-sourced under GPL upon distribution.(&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;)&#10;If your Python application imports or depends on a GPL package, your code must adopt GPL licensing.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;LGPL (Lesser GPL)&lt;/strong&gt;:&#10;LGPL is more permissive and is usually applied to libraries.&#10;It allows proprietary software to link or import the library without forcing the proprietary software itself to adopt LGPL.(&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;)&#10;You just need to ensure users can replace or modify the LGPL component.&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;AGPL (Affero GPL)&lt;/strong&gt;:&#10;AGPL is similar to GPLv3 but includes a critical extra clause: it applies even when users access the software remotely over a network (such as web services or APIs).&#10;If you use an AGPL-licensed Python library, your entire application must be open-sourced under AGPL, even if you&amp;rsquo;re only hosting it online.(&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="what-does-linking-mean-in-python"&gt;What Does &amp;ldquo;Linking&amp;rdquo; Mean in Python? &lt;a class="anchor" href="#what-does-linking-mean-in-python" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;GPL licenses often refer to &amp;ldquo;linking,&amp;rdquo; traditionally associated with compiled languages (e.g., C, C++).&#10;Python doesn&amp;rsquo;t link code in the traditional sense.&#10;However, it is generally agreed upon that importing a Python module counts as linking for licensing purposes.(&lt;sup id="fnref1:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;)&#10;Therefore, directly importing a GPL-licensed Python package typically triggers the GPL&amp;rsquo;s copyleft requirements.&lt;/p&gt;&#10;&lt;h3 id="why-does-this-matter-for-python-packaging"&gt;Why Does This Matter for Python Packaging? &lt;a class="anchor" href="#why-does-this-matter-for-python-packaging" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Python makes it easy to incorporate third-party libraries.&#10;But unintentionally using GPL-licensed libraries (especially GPL or AGPL) legally obligates you to open-source your proprietary code.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Direct Dependencies&lt;/strong&gt;:&#10;If you directly import a GPL-licensed Python library, your software must be GPL-licensed upon distribution.(&lt;sup id="fnref2:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref1:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Indirect (Transitive) Dependencies&lt;/strong&gt;:&#10;If any dependency in your Python project indirectly relies on GPL-licensed code, your entire software must also adopt GPL licensing upon distribution.&#10;Even a hidden, indirect GPL dependency triggers the GPL requirements.(&lt;sup id="fnref:8"&gt;&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref"&gt;8&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref:9"&gt;&lt;a href="#fn:9" class="footnote-ref" role="doc-noteref"&gt;9&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref:10"&gt;&lt;a href="#fn:10" class="footnote-ref" role="doc-noteref"&gt;10&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="recommendations"&gt;Recommendations &lt;a class="anchor" href="#recommendations" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Check Licenses Carefully&lt;/strong&gt;:&#10;Always verify the licenses of both your direct and indirect dependencies to prevent unintended GPL licensing obligations.(&lt;sup id="fnref:11"&gt;&lt;a href="#fn:11" class="footnote-ref" role="doc-noteref"&gt;11&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref1:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Use Permissive or LGPL Licenses&lt;/strong&gt;:&#10;When planning to keep your own software proprietary, prioritize libraries with permissive licenses (e.g., MIT, Apache, BSD) or LGPL-licensed libraries, as they allow for proprietary use without triggering GPL requirements.(&lt;sup id="fnref1:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref1:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;&lt;strong&gt;Separate GPL Code&lt;/strong&gt;:&#10;If you must utilize GPL code, explicitly separate it from proprietary code.&#10;Using subprocess calls or isolated microservices is often recommended since it counts as &amp;ldquo;mere aggregation,&amp;rdquo; not linking, thus avoiding GPL obligations.(&lt;sup id="fnref:12"&gt;&lt;a href="#fn:12" class="footnote-ref" role="doc-noteref"&gt;12&lt;/a&gt;&lt;/sup&gt;,&lt;sup id="fnref2:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;By clearly understanding GPL licensing implications, you can confidently manage your Python project&amp;rsquo;s dependencies and safely distribute your software.&lt;/p&gt;&#10;&lt;!-- Footnotes --&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;&lt;a href="https://www.gnu.org/licenses/gpl-faq.html#IfLibraryIsGPL" target="_blank" rel="noopener"&gt;GNU GPL FAQ - If a library is released under the GPL&lt;/a&gt;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref2:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;&lt;a href="https://opensource.stackexchange.com/questions/35/if-i-include-some-gpl-code-in-my-project-can-i-release-it-as-bsd" target="_blank" rel="noopener"&gt;Stack Exchange - If I include some GPL code in my project, can I release it as BSD?&lt;/a&gt;&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:3"&gt;&#10;&lt;p&gt;&lt;a href="https://www.gnu.org/licenses/lgpl-3.0.en.html" target="_blank" rel="noopener"&gt;GNU Lesser General Public License&lt;/a&gt;&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:4"&gt;&#10;&lt;p&gt;&lt;a href="https://opensource.stackexchange.com/questions/6831/can-i-use-gpl-3-0-or-lgpl-licensed-libraries-in-my-commercial-closed-source-iot" target="_blank" rel="noopener"&gt;Stack Exchange - Can I use GPL/LGPL libraries in commercial closed source projects?&lt;/a&gt;&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:5"&gt;&#10;&lt;p&gt;&lt;a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank" rel="noopener"&gt;GNU Affero General Public License&lt;/a&gt;&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:6"&gt;&#10;&lt;p&gt;&lt;a href="https://opensource.stackexchange.com/questions/11826/can-i-use-a-agpl-gpl-package-and-software-for-closed-source-backend-api" target="_blank" rel="noopener"&gt;Stack Exchange - Can I use AGPL/GPL package for closed source backend API?&lt;/a&gt;&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:7"&gt;&#10;&lt;p&gt;&lt;a href="https://opensource.stackexchange.com/questions/6033/can-a-non-gpl-python-program-use-gpl-python-module" target="_blank" rel="noopener"&gt;Stack Exchange - Can a non-GPL Python program use a GPL Python module?&lt;/a&gt;&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:7" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref2:7" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:8"&gt;&#10;&lt;p&gt;&lt;a href="https://www.gnu.org/licenses/gpl-faq.html#AllCompatibility" target="_blank" rel="noopener"&gt;GNU GPL FAQ - License Compatibility&lt;/a&gt;&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:9"&gt;&#10;&lt;p&gt;&lt;a href="https://dev.to/wagenrace/8000-python-packages-might-have-to-change-to-gnu-general-public-license-1000" target="_blank" rel="noopener"&gt;Dev.to - 8000 Python packages might have to change to GNU General Public License&lt;/a&gt;&amp;#160;&lt;a href="#fnref:9" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:10"&gt;&#10;&lt;p&gt;&lt;a href="https://stackoverflow.com/questions/6748333/import-a-library-that-imports-a-gpl-library" target="_blank" rel="noopener"&gt;Stack Overflow - Import a library that imports a GPL library&lt;/a&gt;&amp;#160;&lt;a href="#fnref:10" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:11"&gt;&#10;&lt;p&gt;&lt;a href="https://www.gnu.org/licenses/license-list.html" target="_blank" rel="noopener"&gt;GNU License List&lt;/a&gt;&amp;#160;&lt;a href="#fnref:11" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:12"&gt;&#10;&lt;p&gt;&lt;a href="https://www.gnu.org/licenses/gpl-faq.html#MereAggregation" target="_blank" rel="noopener"&gt;GNU GPL FAQ - What is &amp;ldquo;mere aggregation&amp;rdquo;?&lt;/a&gt;&amp;#160;&lt;a href="#fnref:12" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>tuitorial: terminal-based interactive code tutorials 📚</title><link>https://www.nijho.lt/post/tuitorial/</link><pubDate>Sat, 04 Jan 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/tuitorial/</guid><description>&lt;p&gt;While trying to demonstrate my &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt; package&lt;/a&gt; (a tool for automating computational pipelines), I hit a common presentation problem: how do you effectively walk through code without endless repetition?&lt;/p&gt;&#10;&lt;p&gt;Traditional presentation tools like PowerPoint force you to copy-paste the same code snippets multiple times just to highlight different parts. Jupyter notebooks are great for self-paced learning but clunky for live presentations (too much text that the viewer will try to read). I needed something that would let me define code once and create multiple views highlighting different aspects.&lt;/p&gt;&#10;&lt;p&gt;So, during the holiday season, I did what any reasonable developer would do: procrastinated on my actual work and built a new tool instead!&lt;/p&gt;&#10;&lt;h2 id="what-is-tuitorial"&gt;What is tuitorial? &lt;a class="anchor" href="#what-is-tuitorial" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/basnijholt/tuitorial" target="_blank" rel="noopener"&gt;&lt;code&gt;tuitorial&lt;/code&gt;&lt;/a&gt; is a Python package that lets you create interactive, terminal-based code tutorials where you define the code ONCE, then create steps that highlight different parts based on rules (like regex patterns or line numbers). No more copy-paste presentations!&lt;/p&gt;&#10;&lt;h2 id="key-features"&gt;Key Features &lt;a class="anchor" href="#key-features" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Write code once&lt;/strong&gt;: Define your code once, then create multiple views highlighting different aspects&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Rich highlighting options&lt;/strong&gt;: From simple literal matches to complex regex patterns&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Interactive navigation&lt;/strong&gt;: Intuitive keyboard controls for stepping through tutorials&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Beautiful terminal UI&lt;/strong&gt;: Powered by Textual for a polished look&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Flexible configuration&lt;/strong&gt;: Choose between Python API or YAML format&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Development mode&lt;/strong&gt;: Live reloading for quick iterations&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="see-it-in-action"&gt;See It in Action &lt;a class="anchor" href="#see-it-in-action" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s a demo of &lt;a href="https://github.com/basnijholt/tuitorial" target="_blank" rel="noopener"&gt;&lt;code&gt;tuitorial&lt;/code&gt;&lt;/a&gt; explaining &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt;&lt;/a&gt; - a package that lets you create computational pipelines with automatic parallelization, caching, and dependency management:&lt;/p&gt;&#10;&#10;&lt;script type="module" src="https://www.nijho.lt/js/reduced-motion.3dfea0061c17c60a9a200c61ef1e3b3e98d5bdc4eaa4e0a64b3ed2a4d13c72fd.js" integrity="sha256-Pf6gBhwXxgqaIAxh7x47PpjVvcTqpOCmSz7SpNE8cv0=" crossorigin="anonymous"&gt;&lt;/script&gt;&#10;&lt;video controls autoplay muted playsinline loop preload="metadata"&gt;&#10;&lt;source src="https://www.nijho.lt/post/tuitorial/tuitorial-0.4.0.mp4" type="video/mp4"&gt;&#10;&lt;/video&gt;&#10;&#10;&lt;h2 id="who-should-use-this"&gt;Who Should Use This? &lt;a class="anchor" href="#who-should-use-this" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While &lt;a href="https://github.com/basnijholt/tuitorial" target="_blank" rel="noopener"&gt;&lt;code&gt;tuitorial&lt;/code&gt;&lt;/a&gt; isn&amp;rsquo;t for everyone, it&amp;rsquo;s perfect for:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Developers giving technical presentations&lt;/li&gt;&#10;&lt;li&gt;Teachers explaining code concepts&lt;/li&gt;&#10;&lt;li&gt;Teams creating interactive documentation&lt;/li&gt;&#10;&lt;li&gt;Anyone who loves terminal-based tools&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="getting-started"&gt;Getting Started &lt;a class="anchor" href="#getting-started" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;Have &lt;code&gt;uv&lt;/code&gt; installed? Run the following command to see a quick example:&#10;&lt;code&gt;uvx tuitorial https://raw.githubusercontent.com/basnijholt/tuitorial/refs/heads/main/examples/pipefunc.yaml&lt;/code&gt;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pip install tuitorial&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Check out the &lt;a href="https://github.com/basnijholt/tuitorial" target="_blank" rel="noopener"&gt;documentation&lt;/a&gt; for examples and detailed usage instructions.&lt;/p&gt;&#10;&lt;h2 id="final-thoughts"&gt;Final Thoughts &lt;a class="anchor" href="#final-thoughts" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Building &lt;a href="https://github.com/basnijholt/tuitorial" target="_blank" rel="noopener"&gt;&lt;code&gt;tuitorial&lt;/code&gt;&lt;/a&gt; was a lot of fun!&#10;While it may not replace PowerPoint for your next business presentation, it offers a unique approach to code demonstrations that I hope others will find useful.&lt;/p&gt;&#10;&lt;p&gt;Try it out and let me know what you think!&lt;/p&gt;&#10;</description></item><item><title>Python environments that stuck: micromamba and uv 🐍</title><link>https://www.nijho.lt/post/python-environments/</link><pubDate>Fri, 03 Jan 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/python-environments/</guid><description>&lt;h2 id="1-introduction-"&gt;1. Introduction 🚀 &lt;a class="anchor" href="#1-introduction-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Like many Python developers, I&amp;rsquo;ve spent considerable time trying different environment management tools to find what works best for me.&lt;/p&gt;&#10;&lt;p&gt;After exploring various options from virtualenv to Conda, I found that Conda-based solutions fit my needs well - mainly because they handle non-Python dependencies without much hassle.&lt;/p&gt;&#10;&lt;p&gt;While Conda itself can be slow when setting up environments, tools like Mamba and Pixi provide the same functionality with better performance.&lt;/p&gt;&#10;&lt;p&gt;In this post, I&amp;rsquo;ll share my experience with different Python environment tools and provide practical commands for setting up Python environments.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;TL;DR, I use &lt;code&gt;micromamba&lt;/code&gt; for projects with non-Python dependencies and &lt;code&gt;uv&lt;/code&gt; for projects without them.&#10;This approach works well for me, you own mileage may vary.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-introduction-"&gt;1. Introduction 🚀&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-the-alternatives-"&gt;2. The Alternatives 🌟&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#-pyenv"&gt;🐍 Pyenv&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-pipenv"&gt;📦 Pipenv&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-conda"&gt;🌐 Conda&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-mamba"&gt;🚀 Mamba&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-micromamba"&gt;🌌 Micromamba&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-docker"&gt;🐳 Docker&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-venv"&gt;🏞️ Venv&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-virtualenv"&gt;🌐 Virtualenv&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-homebrew"&gt;🍺 Homebrew&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-system-package-managers"&gt;💻 System package managers&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-pixi"&gt;🌟 Pixi&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-uv"&gt;⚡ uv&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-conclusion-"&gt;3. Conclusion 🏁&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-further-reading-"&gt;4. Further Reading 📚&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="2-the-alternatives-"&gt;2. The Alternatives 🌟 &lt;a class="anchor" href="#2-the-alternatives-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Before we start look at this comparison tables for the most popular Python environment management tools.&lt;/p&gt;&#10;&lt;p&gt;&lt;em&gt;I had to split the tables into two because of the number of columns.&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Table 1: Python Environment Management Tools&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;div class="table-wrap"&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;Tool&lt;/th&gt;&#10;&lt;th&gt;Key Features&lt;/th&gt;&#10;&lt;th&gt;Native/3rd Party&lt;/th&gt;&#10;&lt;th&gt;Non-Python Deps&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/pyenv/pyenv" target="_blank" rel="noopener"&gt;Pyenv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Python version management&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;❌&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://pipenv.pypa.io/en/latest/" target="_blank" rel="noopener"&gt;Pipenv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Package &amp;amp; env management&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;❌&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://docs.conda.io/en/latest/" target="_blank" rel="noopener"&gt;Conda&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Package &amp;amp; env management&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://mamba.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Mamba&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Fast Conda replacement&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html" target="_blank" rel="noopener"&gt;Micromamba&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Lightweight Mamba&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://www.docker.com/" target="_blank" rel="noopener"&gt;Docker&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Containerization&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://docs.python.org/3/library/venv.html" target="_blank" rel="noopener"&gt;Venv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Built-in env management&lt;/td&gt;&#10;&lt;td&gt;Native&lt;/td&gt;&#10;&lt;td&gt;❌&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://virtualenv.pypa.io/en/latest/" target="_blank" rel="noopener"&gt;Virtualenv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Env management (pre-3.3)&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;❌&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://brew.sh/" target="_blank" rel="noopener"&gt;Homebrew&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;macOS/Linux package manager&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://prefix.dev/docs/pixi/overview" target="_blank" rel="noopener"&gt;Pixi&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Fast Conda alternative&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/astral-sh/uv" target="_blank" rel="noopener"&gt;uv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Fast pip and venv replacement&lt;/td&gt;&#10;&lt;td&gt;3rd Party&lt;/td&gt;&#10;&lt;td&gt;❌&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;System Package Managers&lt;/td&gt;&#10;&lt;td&gt;OS-based package management&lt;/td&gt;&#10;&lt;td&gt;Native&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Table 2: Python Environment Management Tools&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;div class="table-wrap"&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;Tool&lt;/th&gt;&#10;&lt;th&gt;Ease of Use&lt;/th&gt;&#10;&lt;th&gt;Isolation&lt;/th&gt;&#10;&lt;th&gt;Cross-Platform Support&lt;/th&gt;&#10;&lt;th&gt;Community &amp;amp; Support&lt;/th&gt;&#10;&lt;th&gt;Performance&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/pyenv/pyenv" target="_blank" rel="noopener"&gt;Pyenv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Medium&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://pipenv.pypa.io/en/latest/" target="_blank" rel="noopener"&gt;Pipenv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://docs.conda.io/en/latest/" target="_blank" rel="noopener"&gt;Conda&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Excellent&lt;/td&gt;&#10;&lt;td&gt;Average&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://mamba.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Mamba&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Growing&lt;/td&gt;&#10;&lt;td&gt;Fast&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html" target="_blank" rel="noopener"&gt;Micromamba&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Medium&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Growing&lt;/td&gt;&#10;&lt;td&gt;Fast&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://www.docker.com/" target="_blank" rel="noopener"&gt;Docker&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Hard&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Excellent&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://docs.python.org/3/library/venv.html" target="_blank" rel="noopener"&gt;Venv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Excellent&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://virtualenv.pypa.io/en/latest/" target="_blank" rel="noopener"&gt;Virtualenv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Medium&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://brew.sh/" target="_blank" rel="noopener"&gt;Homebrew&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;❌&lt;/td&gt;&#10;&lt;td&gt;macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Excellent&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://prefix.dev/docs/pixi/overview" target="_blank" rel="noopener"&gt;Pixi&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Growing&lt;/td&gt;&#10;&lt;td&gt;Fast&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;&lt;a href="https://github.com/astral-sh/uv" target="_blank" rel="noopener"&gt;uv&lt;/a&gt;&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;✅&lt;/td&gt;&#10;&lt;td&gt;Windows, macOS, Linux&lt;/td&gt;&#10;&lt;td&gt;Growing&lt;/td&gt;&#10;&lt;td&gt;Very Fast&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;System Package Managers&lt;/td&gt;&#10;&lt;td&gt;Easy&lt;/td&gt;&#10;&lt;td&gt;❌&lt;/td&gt;&#10;&lt;td&gt;Depends on OS&lt;/td&gt;&#10;&lt;td&gt;Depends on OS&lt;/td&gt;&#10;&lt;td&gt;Good&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Now that we&amp;rsquo;ve seen the alternatives, let&amp;rsquo;s dive into setting up environments with the help of Conda and Mamba.&lt;/p&gt;&#10;&lt;h3 id="-pyenv"&gt;🐍 Pyenv &lt;a class="anchor" href="#-pyenv" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;A popular tool that enables you to easily manage multiple Python versions on a single system.&#10;It allows you to switch between different Python versions without interfering with system-level installations.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Install pyenv:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;macOS: &lt;code&gt;brew install pyenv&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Ubuntu: &lt;code&gt;curl https://pyenv.run | bash&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Add the following lines to your shell configuration file (e.g., &lt;code&gt;~/.bashrc&lt;/code&gt;, &lt;code&gt;~/.zshrc&lt;/code&gt;):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.pyenv/bin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pyenv init -&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Restart your shell or run &lt;code&gt;source ~/.bashrc&lt;/code&gt; (or &lt;code&gt;source ~/.zshrc&lt;/code&gt;)&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Install a specific Python version: &lt;code&gt;pyenv install 3.8.5&lt;/code&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Set the Python version for your project: &lt;code&gt;pyenv local 3.8.5&lt;/code&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="-pipenv"&gt;📦 Pipenv &lt;a class="anchor" href="#-pipenv" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;A package management and virtual environment management tool that combines the functionality of pip and virtualenv.&#10;It streamlines the process of installing and managing packages within isolated environments.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Install pipenv: &lt;code&gt;pip install --user pipenv&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Change to your project directory: &lt;code&gt;cd my_project&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Initialize a new virtual environment: &lt;code&gt;pipenv --python 3.8&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Install packages: &lt;code&gt;pipenv install &amp;lt;package_name&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Activate the environment: &lt;code&gt;pipenv shell&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="-conda"&gt;🌐 Conda &lt;a class="anchor" href="#-conda" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;A cross-platform package manager and environment management tool, often used in conjunction with Anaconda and Miniconda.&#10;Conda simplifies the installation and management of packages and dependencies, especially for data science and machine learning projects.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Download and install Miniconda: &lt;a href="https://github.com/conda-forge/miniforge#miniforge3" target="_blank" rel="noopener"&gt;https://github.com/conda-forge/miniforge#miniforge3&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Create a new environment: &lt;code&gt;conda create --name my_env python=3.8&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Activate the environment: &lt;code&gt;conda activate my_env&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Install packages: &lt;code&gt;conda install &amp;lt;package_name&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="-mamba"&gt;🚀 Mamba &lt;a class="anchor" href="#-mamba" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Mamba is a fast, drop-in replacement for Conda, designed to resolve and install packages more quickly.&#10;It uses the same Conda repositories and environment files, making it easy to switch between the two.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Install Mambaforge with Mamba pre-installed:&lt;/p&gt;&#10;&lt;p&gt;Download and install Mambaforge with Mamba for your platform: &lt;a href="https://github.com/conda-forge/miniforge#mambaforge" target="_blank" rel="noopener"&gt;https://github.com/conda-forge/miniforge#mambaforge&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;Alternatively, you can install Mamba within an existing Conda environment:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;conda install -c conda-forge mamba&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Create a new environment with Mamba:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mamba create --name my_env &lt;span class="nv"&gt;python&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3.8&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Activate the environment:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mamba activate my_env&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Install packages using Mamba:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mamba install &amp;lt;package_name&amp;gt; -c conda-forge&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;By using Mamba instead of Conda, you can significantly speed up environment resolution and package installation while still benefiting from the Conda ecosystem.&lt;/p&gt;&#10;&lt;h3 id="-micromamba"&gt;🌌 Micromamba &lt;a class="anchor" href="#-micromamba" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Micromamba is a lightweight and fast alternative to the Mamba package manager.&#10;It is a statically linked C++ executable that doesn&amp;rsquo;t require a base environment or come with a default Python version.&#10;It supports a subset of Mamba and Conda commands and is designed to resolve and install packages quickly.&#10;For those who need Conda features, you can install Conda using Micromamba with &lt;code&gt;micromamba install conda&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Download the appropriate Micromamba binary for your platform from &lt;a href="https://github.com/mamba-org/mamba/releases" target="_blank" rel="noopener"&gt;the official repository&lt;/a&gt;.&lt;/li&gt;&#10;&lt;li&gt;Make the binary executable and move it to a directory in your &lt;code&gt;PATH&lt;/code&gt;:&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod +x micromamba mv micromamba /usr/local/bin/&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;Create a new environment with Micromamba:&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;micromamba create -n my_env python -c conda-forge&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;Activate the environment:&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;micromamba activate my_env&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;Install packages using Micromamba:&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;micromamba install &amp;lt;package_name&amp;gt; -c conda-forge&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;If you need Conda&amp;rsquo;s features, you can install Conda within your Micromamba environment:&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;micromamba install conda&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;For example, if you need nb_conda_kernels to use your Micromamba environment in JupyterLab:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;micromamba install conda nb_conda_kernels&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And you are set up!&lt;/p&gt;&#10;&lt;p&gt;Micromamba is the perfect solution for those who want a fast and lightweight package manager while still being able to access Conda&amp;rsquo;s features when needed.&lt;/p&gt;&#10;&lt;h3 id="-docker"&gt;🐳 Docker &lt;a class="anchor" href="#-docker" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;A containerization platform that allows you to create lightweight, portable environments called containers.&#10;With Docker, you can package your Python application along with its dependencies, ensuring consistent execution across different systems.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Install Docker: &lt;a href="https://docs.docker.com/get-docker/" target="_blank" rel="noopener"&gt;https://docs.docker.com/get-docker/&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Create a &lt;code&gt;Dockerfile&lt;/code&gt; in your project directory with the following contents:&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;FROM python:3.11&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WORKDIR /app&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;COPY requirements.txt .&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RUN pip install --no-cache-dir -r requirements.txt&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;COPY . .&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;CMD &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;python&amp;#34;&lt;/span&gt;, &lt;span class="s2"&gt;&amp;#34;your_script.py&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;Build the Docker image: &lt;code&gt;docker build -t my_project .&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Run the Docker container: &lt;code&gt;docker run my_project&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="-venv"&gt;🏞️ Venv &lt;a class="anchor" href="#-venv" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;A built-in Python module available since Python 3.3, which allows you to create lightweight virtual environments.&#10;Venv simplifies the process of creating isolated environments for different projects, ensuring that package dependencies don&amp;rsquo;t conflict with each other.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Create a new virtual environment: &lt;code&gt;python3 -m venv my_env&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Activate the environment:&#10;&lt;ul&gt;&#10;&lt;li&gt;macOS/Linux: &lt;code&gt;source my_env/bin/activate&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Windows: &lt;code&gt;my_envScriptsactivate&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Install packages: &lt;code&gt;pip install &amp;lt;package_name&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="-virtualenv"&gt;🌐 Virtualenv &lt;a class="anchor" href="#-virtualenv" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Virtualenv is a third-party Python environment management tool that predates the built-in &lt;code&gt;venv&lt;/code&gt; module. It allows you to create isolated environments for different projects, ensuring that package dependencies don&amp;rsquo;t conflict with each other. Virtualenv is compatible with Python 2.7 and later versions, whereas &lt;code&gt;venv&lt;/code&gt; is only available since Python 3.3.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Install virtualenv: &lt;code&gt;pip install virtualenv&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Create a new virtual environment: &lt;code&gt;virtualenv my_env&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Activate the environment:&#10;&lt;ul&gt;&#10;&lt;li&gt;macOS/Linux: &lt;code&gt;source my_env/bin/activate&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Windows: &lt;code&gt;my_env\Scripts\activate&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Install packages: &lt;code&gt;pip install &amp;lt;package_name&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="-homebrew"&gt;🍺 Homebrew &lt;a class="anchor" href="#-homebrew" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Homebrew is a popular package manager for macOS and Linux that simplifies the installation and management of software. Though it is not a dedicated Python environment management tool, it does offer support for installing and managing multiple Python versions alongside other software.&lt;/p&gt;&#10;&lt;p&gt;Homebrew can be useful for installing and managing Python versions for system-level usage, but it&amp;rsquo;s not the best choice for managing isolated project-specific dependencies. For project-specific dependency management, it&amp;rsquo;s recommended to use other tools like venv, virtualenv, or Conda in conjunction with Homebrew-installed Python.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Install Homebrew (if not already installed):&#10;&lt;ul&gt;&#10;&lt;li&gt;macOS: Follow the instructions at &lt;a href="https://brew.sh/" target="_blank" rel="noopener"&gt;https://brew.sh&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Linux: Follow the instructions at &lt;a href="https://docs.brew.sh/Homebrew-on-Linux" target="_blank" rel="noopener"&gt;https://docs.brew.sh/Homebrew-on-Linux&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Install a specific Python version:&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew install python@3.11&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;Add the installed Python version to your &lt;code&gt;PATH&lt;/code&gt; by adding the following line to your shell configuration file (e.g., &lt;code&gt;~/.bashrc&lt;/code&gt;, &lt;code&gt;~/.zshrc&lt;/code&gt;):&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/usr/local/opt/python@3.11/bin:&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;Restart your shell or run &lt;code&gt;source ~/.bashrc&lt;/code&gt; (or &lt;code&gt;source ~/.zshrc&lt;/code&gt;) to apply the changes.&lt;/li&gt;&#10;&lt;li&gt;Verify the Python version: &lt;code&gt;python3.11 --version&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Keep in mind that Homebrew is mainly for installing and managing software at the system level. To manage project-specific dependencies and create isolated environments, combine Homebrew with other tools like venv or virtualenv for better dependency management.&lt;/p&gt;&#10;&lt;h3 id="-system-package-managers"&gt;💻 System package managers &lt;a class="anchor" href="#-system-package-managers" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Some operating systems have their own package managers (e.g., apt for Ubuntu, pacman for Arch Linux, or Homebrew for macOS) that can be used to install and manage Python environments.&#10;However, using system package managers may lead to conflicts with other system packages and is generally not recommended for managing project-specific dependencies.&lt;/p&gt;&#10;&lt;p&gt;I will skip the setup instructions for this one, as it is just not recommended to do this!&lt;/p&gt;&#10;&lt;h3 id="-pixi"&gt;🌟 Pixi &lt;a class="anchor" href="#-pixi" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;Pixi is a new package manager built in Rust that uses Conda-forge packages but aims to be significantly faster and simpler than Conda.&#10;It&amp;rsquo;s developed by prefix.dev (the team behind Mamba) and focuses on providing a more streamlined experience.&#10;It is inspired by tools like Cargo, nix, and npm and aims to provide a modern package management experience for Python developers.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Install pixi following instructions at &lt;a href="https://prefix.dev/docs/pixi/installation" target="_blank" rel="noopener"&gt;prefix.dev&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Initialize a new project:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pixi init my-project&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; my-project&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Add dependencies:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pixi add python numpy pandas&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Run Python in the environment:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pixi run python&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Create and run tasks (defined in pixi.toml):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Add a task&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pixi task add hello &lt;span class="s2"&gt;&amp;#34;python hello_world.py&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Run the task&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pixi run hello&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;&#10;&lt;p&gt;Start an interactive shell in the environment:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pixi shell&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Pixi can also be used as a global installation tool:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Install tools globally&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pixi global install starship&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pixi global install fish&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Create a named environment with multiple packages&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pixi global install --environment data-science-env &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --expose python --expose jupyter &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; python jupyter numpy pandas&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Key features:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Fast package resolution and installation&lt;/li&gt;&#10;&lt;li&gt;Uses Conda-forge packages&lt;/li&gt;&#10;&lt;li&gt;Built-in task runner&lt;/li&gt;&#10;&lt;li&gt;Global package installation support&lt;/li&gt;&#10;&lt;li&gt;GitHub Actions integration with automatic caching&lt;/li&gt;&#10;&lt;li&gt;Simple and intuitive CLI&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="-uv"&gt;⚡ uv &lt;a class="anchor" href="#-uv" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;uv is an extremely fast Python package installer and resolver written in Rust.&#10;It can be used as a drop-in replacement for pip and can generate virtual environments.&#10;While it is quite new it is already being used by many developers due to its speed and ease of use.&#10;Besides being fast, it is awesome because it is a single binary that can be used to also bootstrap Python itself.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setup Instructions&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;a href="https://docs.astral.sh/uv/getting-started/installation/" target="_blank" rel="noopener"&gt;Install uv&lt;/a&gt;:&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -LsSf https://astral.sh/uv/install.sh &lt;span class="p"&gt;|&lt;/span&gt; sh&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;Create a new virtual environment:&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uv venv&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;li&gt;Activate the environment:&#10;&lt;ul&gt;&#10;&lt;li&gt;Unix/macOS: &lt;code&gt;source .venv/bin/activate&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Windows: &lt;code&gt;.venv\Scripts\activate&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Install packages (much faster than pip):&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;uv pip install numpy pandas&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;It will automatically maintain a &lt;code&gt;uv.lock&lt;/code&gt; file to ensure deterministic builds.&#10;Install this lock file with &lt;code&gt;uv sync&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;Key features of uv:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Up to 10x faster than pip&lt;/li&gt;&#10;&lt;li&gt;Built-in virtual environment support&lt;/li&gt;&#10;&lt;li&gt;Compatible with pip&amp;rsquo;s CLI interface&lt;/li&gt;&#10;&lt;li&gt;Global package caching&lt;/li&gt;&#10;&lt;li&gt;Deterministic builds&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;There is much more to uv, so check out the &lt;a href="https://docs.astral.sh/uv/" target="_blank" rel="noopener"&gt;documentation&lt;/a&gt; for more details.&lt;/p&gt;&#10;&lt;h2 id="3-conclusion-"&gt;3. Conclusion 🏁 &lt;a class="anchor" href="#3-conclusion-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The choice you make for managing your Python environments ultimately depends on your use case.&#10;If you don&amp;rsquo;t have to frequently deal with non-Python dependencies, I would recommend using &lt;code&gt;uv&lt;/code&gt; for its speed and simplicity.&#10;However, because I often work with projects that require non-Python dependencies, I&amp;rsquo;ve found that Micromamba is the perfect fit for me.&lt;/p&gt;&#10;&lt;p&gt;That being said, each tool has its own strengths and weaknesses, and it&amp;rsquo;s important to choose the one that works best for your specific needs.&#10;I hope this journey through Python environment management has been helpful and informative.&#10;Good luck in finding your perfect Python environment workflow! 🐍&lt;/p&gt;&#10;&lt;h2 id="4-further-reading-"&gt;4. Further Reading 📚 &lt;a class="anchor" href="#4-further-reading-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Here are some resources for further exploration:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/pyenv/pyenv" target="_blank" rel="noopener"&gt;&lt;strong&gt;Pyenv&lt;/strong&gt;&lt;/a&gt;: Pyenv GitHub repository and documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://pipenv.pypa.io/en/latest/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Pipenv&lt;/strong&gt;&lt;/a&gt;: Pipenv documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://docs.conda.io/en/latest/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Conda&lt;/strong&gt;&lt;/a&gt;: Conda documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://mamba.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Mamba&lt;/strong&gt;&lt;/a&gt;: Mamba documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html" target="_blank" rel="noopener"&gt;&lt;strong&gt;Micromamba&lt;/strong&gt;&lt;/a&gt;: Micromamba documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://docs.python.org/3/library/venv.html" target="_blank" rel="noopener"&gt;&lt;strong&gt;venv&lt;/strong&gt;&lt;/a&gt;: venv documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://virtualenv.pypa.io/en/latest/" target="_blank" rel="noopener"&gt;&lt;strong&gt;virtualenv&lt;/strong&gt;&lt;/a&gt;: virtualenv documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.anaconda.com/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Anaconda&lt;/strong&gt;&lt;/a&gt;: Anaconda homepage&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://docs.conda.io/en/latest/miniconda.html" target="_blank" rel="noopener"&gt;&lt;strong&gt;Miniconda&lt;/strong&gt;&lt;/a&gt;: Miniconda documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://brew.sh/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Homebrew&lt;/strong&gt;&lt;/a&gt;: Homebrew homepage&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://prefix.dev/docs/pixi/overview" target="_blank" rel="noopener"&gt;&lt;strong&gt;Pixi&lt;/strong&gt;&lt;/a&gt;: Pixi documentation&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://docs.astral.sh/uv/" target="_blank" rel="noopener"&gt;&lt;strong&gt;uv&lt;/strong&gt;&lt;/a&gt;: uv documentation&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>Home Assistant Ecosystem 🏠</title><link>https://www.nijho.lt/post/advent-of-open-source/24-home-assistant-ecosystem/</link><pubDate>Tue, 24 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/24-home-assistant-ecosystem/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;There are not enough days in this advent calendar to highlight all my projects. So today is a collection of IoT/Home Assistant-related packages. Each has its own story.&lt;/p&gt;&#10;&lt;h2 id="-origin-stories--technical-details"&gt;📖 Origin Stories &amp;amp; Technical Details &lt;a class="anchor" href="#-origin-stories--technical-details" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My Home Assistant obsession began as described in &lt;a href="https://www.linkedin.com/posts/basnijholt_opensource-homeassistant-smarthome-activity-7272248713882820608-w7RV" target="_blank" rel="noopener"&gt;Day 10&lt;/a&gt;. Many projects emerged from this hobby:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/aiokef" target="_blank" rel="noopener"&gt;&lt;strong&gt;aiokef&lt;/strong&gt;&lt;/a&gt;: To control my KEF speakers, I reverse-engineered network traffic and decoded raw byte sequences, diving deep into proprietary protocols to &amp;ldquo;speak&amp;rdquo; the speaker&amp;rsquo;s language.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/miflora" target="_blank" rel="noopener"&gt;&lt;strong&gt;miflora&lt;/strong&gt;&lt;/a&gt;: When &lt;a href="https://github.com/ChristianKuehnel" target="_blank" rel="noopener"&gt;@ChristianKuehnel&lt;/a&gt; couldn&amp;rsquo;t maintain this plant sensor library anymore, he transferred it to me. A perfect example of open source collaboration, it continues to help users monitor plant health and demonstrates how projects can thrive through community handovers.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/addon-otmonitor" target="_blank" rel="noopener"&gt;&lt;strong&gt;addon-otmonitor&lt;/strong&gt;&lt;/a&gt;: This HA add-on packages OpenTherm Monitor for the OpenTherm Gateway (OTGW). Using Docker to simplify deployment, it&amp;rsquo;s become a powerful open-source alternative to Nest or Ecobee, giving full control over heating systems.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/home-assistant-macbook-touch-bar" target="_blank" rel="noopener"&gt;&lt;strong&gt;home-assistant-macbook-touch-bar&lt;/strong&gt;&lt;/a&gt;: Remember that useless touch bar on the old MacBook? I transformed it into a mini Home Assistant control panel for lights, temperature, and more - a classic case of hardware hacking giving new life to abandoned technology.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/python-kasa/python-kasa" target="_blank" rel="noopener"&gt;&lt;strong&gt;python-kasa&lt;/strong&gt;&lt;/a&gt;: To modernize the popular &lt;code&gt;pyHS100&lt;/code&gt; library for TP-Link devices, I converted it to use &lt;code&gt;asyncio&lt;/code&gt;. With &lt;a href="https://github.com/rytilahti" target="_blank" rel="noopener"&gt;@rytilahti&lt;/a&gt;, we created the &lt;code&gt;python-kasa&lt;/code&gt; organization through community collaboration. While I haven&amp;rsquo;t contributed much since its inception, it&amp;rsquo;s now the standard for controlling Kasa devices.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact--lessons"&gt;📊 Impact &amp;amp; Lessons &lt;a class="anchor" href="#-impact--lessons" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;These projects serve thousands of users while teaching valuable lessons:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Open source empowers us: from KEF speaker integration to plant monitoring&lt;/li&gt;&#10;&lt;li&gt;Reverse engineering unlocks potential: turning proprietary protocols into open solutions&lt;/li&gt;&#10;&lt;li&gt;Community drives success: projects like miflora and python-kasa thrive through collaboration&lt;/li&gt;&#10;&lt;li&gt;Innovation through repurposing: giving new life to hardware like the MacBook Touch Bar&lt;/li&gt;&#10;&lt;li&gt;Modern tech matters: Docker simplifies deployment, async code boosts performance&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Want to explore these projects? Check them out on GitHub:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/aiokef" target="_blank" rel="noopener"&gt;aiokef&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/miflora" target="_blank" rel="noopener"&gt;miflora&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/addon-otmonitor" target="_blank" rel="noopener"&gt;addon-otmonitor&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/home-assistant-macbook-touch-bar" target="_blank" rel="noopener"&gt;home-assistant-macbook-touch-bar&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/python-kasa/python-kasa" target="_blank" rel="noopener"&gt;python-kasa&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This concludes our Advent of Open Source! I hope you&amp;rsquo;ve enjoyed this journey through my projects as much as I&amp;rsquo;ve enjoyed sharing them. Happy holidays and happy coding!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #HomeAssistant #IoT #Python #SmartHome&lt;/p&gt;&#10;</description></item><item><title>tuitorial</title><link>https://www.nijho.lt/project/tuitorial/</link><pubDate>Tue, 24 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/tuitorial/</guid><description/></item><item><title>pfapack 🔢</title><link>https://www.nijho.lt/post/advent-of-open-source/23-pfapack/</link><pubDate>Mon, 23 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/23-pfapack/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Ever needed to calculate the square root of the determinant of a skew-symmetric matrix? Probably not, but if you&amp;rsquo;re in quantum physics, you might have! This is called the Pfaffian. Today&amp;rsquo;s project, &lt;a href="https://github.com/basnijholt/pfapack" target="_blank" rel="noopener"&gt;&lt;code&gt;pfapack&lt;/code&gt;&lt;/a&gt;, is a Python package that provides efficient routines for computing it. The underlying algorithms and original code implementations were developed by my PhD co-promotor Michael Wimmer. I created the &lt;code&gt;pfapack&lt;/code&gt; package to make this powerful tool easily accessible.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Working in quantum physics, I frequently need to compute the Pfaffian. Michael had not only developed the algorithms but also wrote highly optimized C, FORTRAN, and Python implementations, distributing them as a tar file &lt;a href="https://michaelwimmer.org/downloads.html" target="_blank" rel="noopener"&gt;on his website&lt;/a&gt;. To make it easier for researchers working in Python, I packaged his existing Python code, added wrappers for his C and FORTRAN implementations, and set up automated builds for PyPI and conda-forge to make it easily installable.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Python Wrapper for Efficient Code:&lt;/strong&gt; User-friendly Python interface to Wimmer&amp;rsquo;s optimized C and FORTRAN.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Multiple Algorithms:&lt;/strong&gt; Supports both the Parlett-Reid and Householder methods.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Optimized for Performance:&lt;/strong&gt; Leverages low-level optimizations for speed.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Conda and Pip Installable:&lt;/strong&gt; Easy installation via &lt;code&gt;conda-forge&lt;/code&gt; and PyPI.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Automated Wheel Builds:&lt;/strong&gt; Uses &lt;a href="https://github.com/pypa/cibuildwheel" target="_blank" rel="noopener"&gt;&lt;code&gt;pypa/cibuildwheel&lt;/code&gt;&lt;/a&gt; for streamlined, automated builds and releases.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Cross-Platform Compatibility:&lt;/strong&gt; Works on Linux, macOS, and Windows (with some caveats).&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Makes specialized mathematical algorithms readily available to Python users.&lt;/li&gt;&#10;&lt;li&gt;Simplifies the use of highly optimized code.&lt;/li&gt;&#10;&lt;li&gt;15 GitHub stars.&lt;/li&gt;&#10;&lt;li&gt;Used in research projects that require efficient Pfaffian computations, particularly in quantum physics.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Bridging Languages:&lt;/strong&gt; Wrapping C and FORTRAN for Python integration.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Automated Builds:&lt;/strong&gt; Setting up automated builds for multiple platforms was complex.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Windows Compatibility:&lt;/strong&gt; As always Windows support is a PITA; it currently requires MSYS2 and MinGW64.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Making specialized tools accessible broadens their impact.&lt;/li&gt;&#10;&lt;li&gt;Wrapping low-level code in Python can improve usability.&lt;/li&gt;&#10;&lt;li&gt;Automated builds are essential for maintainability.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/pypa/cibuildwheel" target="_blank" rel="noopener"&gt;&lt;code&gt;pypa/cibuildwheel&lt;/code&gt;&lt;/a&gt; is awesome for cross-platform builds.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-credits"&gt;🙏 Credits &lt;a class="anchor" href="#-credits" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;All the credits for the underlying algorithms and original code implementations go to Michael Wimmer. I simply made it easier to distribute and use in Python.&lt;/p&gt;&#10;&lt;p&gt;Want to compute Pfaffians efficiently in Python? Check out &lt;a href="https://github.com/basnijholt/pfapack" target="_blank" rel="noopener"&gt;&lt;code&gt;pfapack&lt;/code&gt; on GitHub&lt;/a&gt; or install it via &lt;code&gt;pip install pfapack&lt;/code&gt; or &lt;code&gt;conda install -c conda-forge pfapack&lt;/code&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #ScientificComputing #Math #Pfaffian #FORTRAN #C&lt;/p&gt;&#10;</description></item><item><title>tiller-streamlit 💰</title><link>https://www.nijho.lt/post/advent-of-open-source/22-tiller-streamlit/</link><pubDate>Sun, 22 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/22-tiller-streamlit/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Tracking personal finances is crucial, especially when working towards financial independence (see my &lt;a href="../04-wenfire"&gt;WenFIRE post on Day 4&lt;/a&gt;). Today&amp;rsquo;s project is &lt;a href="https://github.com/basnijholt/tiller-streamlit" target="_blank" rel="noopener"&gt;&lt;code&gt;tiller-streamlit&lt;/code&gt;&lt;/a&gt;, a Streamlit app that visualizes your financial data from &lt;a href="https://www.tillerhq.com/" target="_blank" rel="noopener"&gt;Tiller&lt;/a&gt;. I host it on my local network under &lt;code&gt;money.local&lt;/code&gt; using Traefik for easy access.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Used to use Intuit Mint, but when they shut down, I needed an alternative with full data control.&lt;/li&gt;&#10;&lt;li&gt;Tiller automatically populates your financial data into a Google Sheet that you own forever.&lt;/li&gt;&#10;&lt;li&gt;With 20+ accounts, manual updates are tedious – Tiller automates it.&lt;/li&gt;&#10;&lt;li&gt;Combined Tiller&amp;rsquo;s data with Streamlit&amp;rsquo;s ease of use, creating a personal finance dashboard.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Powered by Streamlit: Turns Python into a web app with minimal code.&lt;/li&gt;&#10;&lt;li&gt;Tiller Integration: Connects to Tiller&amp;rsquo;s Google Sheets for automated data retrieval.&lt;/li&gt;&#10;&lt;li&gt;Interactive Visualizations: Uses Plotly and Altair for dynamic charts.&lt;/li&gt;&#10;&lt;li&gt;Customizable Analysis: Filter by categories, time periods, and more.&lt;/li&gt;&#10;&lt;li&gt;Net Worth Tracking: Calculates and visualizes net worth over time.&lt;/li&gt;&#10;&lt;li&gt;Dockerized Deployment: Easy deployment with Docker, and served locally with Traefik.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Primarily for personal use, it has significantly improved my understanding of my finances.&lt;/li&gt;&#10;&lt;li&gt;Great way to learn Streamlit and explore its capabilities.&lt;/li&gt;&#10;&lt;li&gt;Visualizing spending patterns helps identify areas to cut back and make informed financial decisions on the path to financial independence.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Learning Streamlit: Excellent documentation and community support made it a smooth process.&lt;/li&gt;&#10;&lt;li&gt;Data Wrangling: Transforming Tiller&amp;rsquo;s raw data into a format suitable for visualization.&lt;/li&gt;&#10;&lt;li&gt;Deployment: Docker and Traefik simplified local deployment, making it accessible on my home network.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Data Ownership Matters: Owning your financial data gives you control and flexibility.&lt;/li&gt;&#10;&lt;li&gt;Visualization is Key: Seeing your finances visually makes a huge difference.&lt;/li&gt;&#10;&lt;li&gt;Streamlit is Powerful: Simplifies web app development.&lt;/li&gt;&#10;&lt;li&gt;Automation is Essential: Tiller&amp;rsquo;s automatic data population makes the whole process sustainable.&lt;/li&gt;&#10;&lt;li&gt;Docker and Traefik Simplify Local Deployment: Making it easy to access the app on my home network.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-future-plans"&gt;🔮 Future Plans &lt;a class="anchor" href="#-future-plans" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Budgeting Features: Integrate budget tracking and forecasting.&lt;/li&gt;&#10;&lt;li&gt;Goal Setting: Set financial goals and track progress.&lt;/li&gt;&#10;&lt;li&gt;Investment Tracking: Incorporate investment data for a complete picture.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Want to visualize your own finances with Tiller and Streamlit? Check out &lt;a href="https://github.com/basnijholt/tiller-streamlit" target="_blank" rel="noopener"&gt;&lt;code&gt;tiller-streamlit&lt;/code&gt; on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #Streamlit #PersonalFinance #DataVisualization #Tiller #Traefik #Docker&lt;/p&gt;&#10;</description></item><item><title>Interactive Graph Visualization 🕸️</title><link>https://www.nijho.lt/post/advent-of-open-source/21-interactive-graph-visualization/</link><pubDate>Sat, 21 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/21-interactive-graph-visualization/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;After creating &lt;a href="../20-pipefunc"&gt;pipefunc (Day 20)&lt;/a&gt;, I wanted a way to visualize its (possibly large and complex) computational graphs interactively in Jupyter notebooks.&#10;What started as a simple visualization PR evolved into two standalone packages: &lt;a href="https://github.com/pipefunc/graphviz-anywidget" target="_blank" rel="noopener"&gt;graphviz-anywidget&lt;/a&gt; (Python) and &lt;a href="https://github.com/pipefunc/graphvizsvg" target="_blank" rel="noopener"&gt;graphvizsvg&lt;/a&gt; (JavaScript) - which unexpectedly introduced me to JavaScript development!&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While working on pipefunc&amp;rsquo;s visualization features, I discovered that existing solutions didn&amp;rsquo;t quite meet my needs.&#10;However, I found several components that would solve my issue:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://anywidget.dev/" target="_blank" rel="noopener"&gt;AnyWidget&lt;/a&gt; (interactive GraphViz in Jupyter)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/hpcc-systems/hpcc-js-wasm" target="_blank" rel="noopener"&gt;@hpcc-js/wasm-graphviz&lt;/a&gt; (GraphViz in WASM)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/magjac/d3-graphviz" target="_blank" rel="noopener"&gt;d3-graphviz&lt;/a&gt; (interactive GraphViz in D3)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/mountainstorm/jquery.graphviz.svg/" target="_blank" rel="noopener"&gt;jquery.graphviz.svg&lt;/a&gt; (GraphViz SVG manipulation)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;AnyWidget would work with ESM modules, however, I had no clue what that even was.&#10;The &lt;code&gt;jquery.graphviz.svg&lt;/code&gt; package wasn&amp;rsquo;t one and hadn&amp;rsquo;t been maintained in 8 years.&lt;/p&gt;&#10;&lt;p&gt;With AI as my JavaScript mentor, I first rewrote &lt;code&gt;jquery.graphviz.svg&lt;/code&gt; into a modern ESM module (graphvizsvg), adding comprehensive tests that uncovered bugs in the original.&#10;Then, using AnyWidget, I created graphviz-anywidget to connect everything together - combining the WASM-powered d3-graphviz with the interactive features from graphvizsvg into a seamless Python package.&lt;/p&gt;&#10;&lt;p&gt;To my surprise, I found JavaScript&amp;rsquo;s development tooling remarkably sophisticated - especially npm, which handles everything from linting to testing to bundling to publishing.&#10;Something we&amp;rsquo;re still missing in Python!&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/pipefunc/graphviz-anywidget" target="_blank" rel="noopener"&gt;graphviz-anywidget&lt;/a&gt;:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Interactive Graphviz visualization in Jupyter&lt;/li&gt;&#10;&lt;li&gt;WASM-powered graph rendering&lt;/li&gt;&#10;&lt;li&gt;Directional graph traversal&lt;/li&gt;&#10;&lt;li&gt;Node and edge highlighting&lt;/li&gt;&#10;&lt;li&gt;Works in JupyterLab, Notebook, and VS Code&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/pipefunc/graphvizsvg" target="_blank" rel="noopener"&gt;graphvizsvg&lt;/a&gt;:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Modern ESM module for interactive SVG visualization&lt;/li&gt;&#10;&lt;li&gt;Automatic color transitions&lt;/li&gt;&#10;&lt;li&gt;Bi-directional graph traversal&lt;/li&gt;&#10;&lt;li&gt;Comprehensive test coverage&lt;/li&gt;&#10;&lt;li&gt;My first npm package!&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Enhances &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;pipefunc&lt;/a&gt;&amp;rsquo;s visualization capabilities&lt;/li&gt;&#10;&lt;li&gt;Modernizes legacy jQuery code for modern web development&lt;/li&gt;&#10;&lt;li&gt;Proves that diving into new technologies can yield useful tools&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Learning JavaScript/TypeScript from scratch&lt;/li&gt;&#10;&lt;li&gt;Making WASM work in Jupyter&lt;/li&gt;&#10;&lt;li&gt;Getting it to work in VS Code&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Don&amp;rsquo;t let preconceptions limit your tech choices&lt;/li&gt;&#10;&lt;li&gt;Modern JavaScript development is actually enjoyable&lt;/li&gt;&#10;&lt;li&gt;AI can be an excellent programming mentor&lt;/li&gt;&#10;&lt;li&gt;Good visualization makes complex data accessible&lt;/li&gt;&#10;&lt;li&gt;Breaking features into separate packages increases reusability&lt;/li&gt;&#10;&lt;li&gt;Test coverage helps find bugs in even well-used code&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to visualize graphs interactively? Check out &lt;a href="https://github.com/pipefunc/graphviz-anywidget" target="_blank" rel="noopener"&gt;graphviz-anywidget&lt;/a&gt; and &lt;a href="https://github.com/pipefunc/graphvizsvg" target="_blank" rel="noopener"&gt;graphvizsvg&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #JavaScript #Python #DataVisualization #WebDev&lt;/p&gt;&#10;</description></item><item><title>pipefunc 🕸️</title><link>https://www.nijho.lt/post/advent-of-open-source/20-pipefunc/</link><pubDate>Fri, 20 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/20-pipefunc/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Of all the projects I&amp;rsquo;m sharing this month, this one that started as a passion project excites me the most!&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Sometimes the best projects are born from the most unexpected moments. More than 1½ year ago, while on parental leave with my twin boys, I found myself with small pockets of time during naps to work on a problem that had been bothering me for years: the tedious bookkeeping required in complex computational workflows. Every scientific computation project seemed to reinvent the same patterns - managing function dependencies, parameter sweeps, result caching, parallelization, and a lot of boilerplate to combine the resulting data. I wanted something that would let scientists focus on their science, not on pipeline management.&lt;/p&gt;&#10;&lt;figure id="figure-example-of-a-pipeline-generated-by-pipefunc"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/20-pipefunc/image_2_hu_91068e582234f921.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/20-pipefunc/image_2_hu_75ce53994a0e90e4.webp 400w, https://www.nijho.lt/post/advent-of-open-source/20-pipefunc/image_2_hu_91068e582234f921.webp 760w, https://www.nijho.lt/post/advent-of-open-source/20-pipefunc/image_2_hu_220beece6a403360.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="383" alt="A screenshot of a code editor showing Python code that uses the &amp;#39;pipefunc&amp;#39; library to define and visualize a computational pipeline. On the right, a graph generated by &amp;#39;pipeline.visualize()&amp;#39; is displayed, showing nodes and connections representing the pipeline&amp;#39;s structure." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Example of a pipeline generated by PipeFunc&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Automatic DAG construction via very simple and lightweight syntax&lt;/li&gt;&#10;&lt;li&gt;N-dimensional parameter sweeps with automatic parallelization&lt;/li&gt;&#10;&lt;li&gt;Visual pipeline representation using NetworkX&lt;/li&gt;&#10;&lt;li&gt;Resource profiling (CPU, memory, time)&lt;/li&gt;&#10;&lt;li&gt;Type validation between pipeline stages&lt;/li&gt;&#10;&lt;li&gt;Ultra-fast: only 15 µs overhead per function&lt;/li&gt;&#10;&lt;li&gt;Flexible caching strategies (memory, disk, cloud)&lt;/li&gt;&#10;&lt;li&gt;Integration with scientific computing tools:&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://networkx.org/" target="_blank" rel="noopener"&gt;NetworkX&lt;/a&gt; for graph algorithms&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://numpy.org/" target="_blank" rel="noopener"&gt;NumPy&lt;/a&gt; for multi-dimensional arrays&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://docs.xarray.dev/" target="_blank" rel="noopener"&gt;Xarray&lt;/a&gt; for labeled arrays&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://zarr.dev/" target="_blank" rel="noopener"&gt;Zarr&lt;/a&gt; for distributed storage&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://adaptive.readthedocs.io/" target="_blank" rel="noopener"&gt;Adaptive&lt;/a&gt; for parallel computing&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;230 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;700+ tests with 100% coverage&lt;/li&gt;&#10;&lt;li&gt;Fully typed codebase&lt;/li&gt;&#10;&lt;li&gt;Comprehensive documentation&lt;/li&gt;&#10;&lt;li&gt;Tested on real workflow on SLURM cluster&lt;/li&gt;&#10;&lt;li&gt;Covered on &lt;a href="https://pycoders.com/issues/650" target="_blank" rel="noopener"&gt;Pycoder&amp;rsquo;s Weekly&lt;/a&gt; with &amp;gt;100k subscribers&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Balancing simplicity with power&lt;/li&gt;&#10;&lt;li&gt;Making complex workflows intuitive&lt;/li&gt;&#10;&lt;li&gt;Handling distributed computing edge cases&lt;/li&gt;&#10;&lt;li&gt;Ensuring type safety across the pipeline&lt;/li&gt;&#10;&lt;li&gt;Optimizing performance without sacrificing features&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Sometimes the best time to code is during baby naps&lt;/li&gt;&#10;&lt;li&gt;Complex problems can have elegant solutions&lt;/li&gt;&#10;&lt;li&gt;Scientific computing needs better tooling&lt;/li&gt;&#10;&lt;li&gt;Good abstractions make hard things easy&lt;/li&gt;&#10;&lt;li&gt;Type hints and tests prevent headaches&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="-future-plans"&gt;🔮 Future Plans &lt;a class="anchor" href="#-future-plans" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The journey is far from over. Plans include:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Enhanced cloud computing support&lt;/li&gt;&#10;&lt;li&gt;More interactive visualization options&lt;/li&gt;&#10;&lt;li&gt;Interactive pipeline debugging tools&lt;/li&gt;&#10;&lt;li&gt;Expanded parameter sweep capabilities&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Want to simplify your computational workflows? Check out &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;pipefunc on GitHub&lt;/a&gt; or read the &lt;a href="https://pipefunc.readthedocs.io/" target="_blank" rel="noopener"&gt;documentation&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #DataScience #ScientificComputing #Programming&lt;/p&gt;&#10;</description></item><item><title>fileup 📤</title><link>https://www.nijho.lt/post/advent-of-open-source/19-fileup/</link><pubDate>Thu, 19 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/19-fileup/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Sometimes the best tools are the ones you build for yourself and keep using for years. Today&amp;rsquo;s project is &lt;a href="https://github.com/basnijholt/fileup" target="_blank" rel="noopener"&gt;&lt;code&gt;fileup&lt;/code&gt;&lt;/a&gt;, a tiny command-line utility that&amp;rsquo;s been making my life easier for 7 years - because sharing files shouldn&amp;rsquo;t require leaving the terminal!&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Back in 2017, I found myself repeatedly going through the same tedious process: needing to share a file, opening a browser, navigating to a file sharing service, waiting for uploads, and copying URLs. As someone who lives in the terminal, this felt inefficient. I wanted something as simple as &lt;code&gt;fu filename&lt;/code&gt; that would handle everything and put the URL in my clipboard. Seven years and countless file shares later, it&amp;rsquo;s still one of my most-used tools.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Supports both FTP and SCP uploads&lt;/li&gt;&#10;&lt;li&gt;Smart URL generation:&#10;&lt;ul&gt;&#10;&lt;li&gt;Jupyter notebooks → nbviewer links&lt;/li&gt;&#10;&lt;li&gt;Images → markdown-formatted links&lt;/li&gt;&#10;&lt;li&gt;Regular files → direct URLs&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Automatic file expiration and cleanup&lt;/li&gt;&#10;&lt;li&gt;Zero dependencies - uses only Python standard library&lt;/li&gt;&#10;&lt;li&gt;Clipboard integration (macOS)&lt;/li&gt;&#10;&lt;li&gt;Simple INI configuration&lt;/li&gt;&#10;&lt;li&gt;Works as both a standalone script or installed package&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This is one of those tools where GitHub stars (currently 0!) don&amp;rsquo;t tell the whole story:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Used almost daily for 7 years&lt;/li&gt;&#10;&lt;li&gt;Shared thousands of files&lt;/li&gt;&#10;&lt;li&gt;Saved countless minutes of browser interactions&lt;/li&gt;&#10;&lt;li&gt;Proves that utility trumps popularity in open source&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Longevity comes from solving a real, persistent need&lt;/li&gt;&#10;&lt;li&gt;A tool doesn&amp;rsquo;t need to be revolutionary to be useful&lt;/li&gt;&#10;&lt;li&gt;Zero dependencies mean zero maintenance headaches&lt;/li&gt;&#10;&lt;li&gt;Sometimes the impact of a tool isn&amp;rsquo;t measured in stars&lt;/li&gt;&#10;&lt;li&gt;Simple tools that remove friction are worth maintaining&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to share files from your terminal? Check out &lt;a href="https://github.com/basnijholt/fileup" target="_blank" rel="noopener"&gt;fileup on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #DevTools #CommandLine #Programming&lt;/p&gt;&#10;</description></item><item><title>Psychedelic Data Science 🍄</title><link>https://www.nijho.lt/post/advent-of-open-source/18-psychedelic-data-science/</link><pubDate>Wed, 18 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/18-psychedelic-data-science/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;📝 Note: Running out of awesome projects and I want to save the best for last, so today just something fun.&lt;/p&gt;&#10;&lt;p&gt;While preparing for this advent calendar, I browsed through my 382 GitHub repositories and rediscovered this time capsule from 8 years ago. It explores the language of psychedelic experiences through data science - with an interesting hypothesis: do psychedelic experiences generate richer vocabulary compared to other substance reports?&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Back in 2015, when I was still &amp;ldquo;young&amp;rdquo; (before back pain and two-day hangovers became a thing), I was learning data science and natural language processing. I had an amusing hypothesis: surely people describing their psychedelic experiences would use richer vocabulary than those writing about stimulants - I mean, who hasn&amp;rsquo;t pondered the linguistic complexity of &amp;ldquo;everything is connected&amp;rdquo; versus &amp;ldquo;I cleaned my entire apartment at 4 AM&amp;rdquo;? Erowid.org, with its thousands of detailed first-person narratives across different substances, provided the perfect dataset to test this theory.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Natural language processing of experience reports&lt;/li&gt;&#10;&lt;li&gt;Vocabulary richness analysis across substance categories&lt;/li&gt;&#10;&lt;li&gt;TF-IDF vectorization for text analysis&lt;/li&gt;&#10;&lt;li&gt;Support Vector Machine classification of experience types&lt;/li&gt;&#10;&lt;li&gt;Web scraping with BeautifulSoup&lt;/li&gt;&#10;&lt;li&gt;K-means clustering for discovering common themes&lt;/li&gt;&#10;&lt;li&gt;Word cloud generation to visualize vocabulary differences&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;A learning project that helped understand:&#10;&lt;ul&gt;&#10;&lt;li&gt;How different experiences shape language use&lt;/li&gt;&#10;&lt;li&gt;Processing subjective experience narratives&lt;/li&gt;&#10;&lt;li&gt;Document classification techniques&lt;/li&gt;&#10;&lt;li&gt;The challenges of quantifying vocabulary richness&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;10 GitHub stars&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Implementing respectful web scraping&lt;/li&gt;&#10;&lt;li&gt;Controlling for report length and education level&lt;/li&gt;&#10;&lt;li&gt;Creating meaningful metrics for vocabulary richness&lt;/li&gt;&#10;&lt;li&gt;Visualizing linguistic patterns across categories&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Putting things on GitHub means it&amp;rsquo;s less likely forgotten things get lost&lt;/li&gt;&#10;&lt;li&gt;Early data science projects often reveal our initial fascinations&lt;/li&gt;&#10;&lt;li&gt;Web scraping requires both technical skill and ethical consideration&lt;/li&gt;&#10;&lt;li&gt;Text analysis tools have evolved dramatically since 2015&lt;/li&gt;&#10;&lt;li&gt;Sometimes the most interesting projects are the ones you almost forgot about&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to explore this intersection of psychedelics, language, and data science? Check out &lt;a href="https://github.com/basnijholt/psychedelic-data-science" target="_blank" rel="noopener"&gt;the project on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #DataScience #NLP #MachineLearning&lt;/p&gt;&#10;</description></item><item><title>Adaptive Scheduler 🚀</title><link>https://www.nijho.lt/post/advent-of-open-source/17-adaptive-scheduler/</link><pubDate>Tue, 17 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/17-adaptive-scheduler/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Ever tried to efficiently run 100,000 cores from a Jupyter notebook? No? Well, I have, and it led to some interesting discoveries about the limits of parallel computing tools.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;During my 2018 internship at Microsoft Quantum, we faced a unique challenge: our &lt;a href="https://github.com/python-adaptive/adaptive" target="_blank" rel="noopener"&gt;adaptive sampling algorithms&lt;/a&gt; needed to run on massive computing clusters (50,000+ cores), but existing tools couldn&amp;rsquo;t handle it efficiently.&lt;/p&gt;&#10;&lt;p&gt;The problem was interesting: traditional parallel computing tools like Dask rely on a central scheduler process. Imagine 100,000 cores, each finishing a task every 10 seconds. That means every 10 microseconds, a core needs new work! With typical scheduler overhead being 1-50 milliseconds, workers would spend 99% of their time waiting rather than computing.&lt;/p&gt;&#10;&lt;p&gt;Usually, you&amp;rsquo;d solve this by batching work ahead of time. But with adaptive sampling (&lt;a href="../16-adaptive"&gt;yesterday&amp;rsquo;s project&lt;/a&gt;), that&amp;rsquo;s impossible - you need the results of previous calculations to know what to compute next. It was like trying to conduct an orchestra where each musician needs personal instructions every few seconds!&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;We created a solution that:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Acts as a meta-scheduler, launching multiple sub-schedulers (&lt;a href="https://www.dask.org/" target="_blank" rel="noopener"&gt;Dask&lt;/a&gt;, &lt;a href="https://ipyparallel.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;ipyparallel&lt;/a&gt;, &lt;a href="https://mpi4py.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;mpi4py&lt;/a&gt;, etc.)&lt;/li&gt;&#10;&lt;li&gt;Minimizes communication between scheduling layers&lt;/li&gt;&#10;&lt;li&gt;Handles job management through a simple database&lt;/li&gt;&#10;&lt;li&gt;Supports automatic fault tolerance and data saving&lt;/li&gt;&#10;&lt;li&gt;Works with any cluster scheduler (SLURM, PBS)&lt;/li&gt;&#10;&lt;li&gt;Provides real-time progress tracking in Jupyter notebooks&lt;/li&gt;&#10;&lt;li&gt;Scales to 50,000+ cores without breaking a sweat&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Used extensively for real quantum device simulations&lt;/li&gt;&#10;&lt;li&gt;26 GitHub stars (but don&amp;rsquo;t let that fool you - it&amp;rsquo;s highly specialized!)&lt;/li&gt;&#10;&lt;li&gt;Enables interactive supercomputing from a simple Jupyter notebook&lt;/li&gt;&#10;&lt;li&gt;Likely ran for many millions of core-hours across various projects&lt;/li&gt;&#10;&lt;li&gt;Actively maintained with 61 releases since 2018, proving its ongoing value&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-we-solved"&gt;🎯 Challenges We Solved &lt;a class="anchor" href="#-challenges-we-solved" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Scheduler overhead killing performance at scale&lt;/li&gt;&#10;&lt;li&gt;Data locality and fault tolerance&lt;/li&gt;&#10;&lt;li&gt;Real-time monitoring of massive parallel jobs&lt;/li&gt;&#10;&lt;li&gt;Making supercomputing accessible through notebooks&lt;/li&gt;&#10;&lt;li&gt;Automatic job recovery after crashes (e.g., when using spot VMs or Heisenbugs)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Sometimes you need to think outside the box&lt;/li&gt;&#10;&lt;li&gt;The best tools hide complexity behind simple interfaces&lt;/li&gt;&#10;&lt;li&gt;Real-time feedback is crucial, even for massive computations&lt;/li&gt;&#10;&lt;li&gt;When scaling up 1000x, you often need a completely different approach&lt;/li&gt;&#10;&lt;li&gt;Making things &amp;ldquo;just work&amp;rdquo; from a notebook is worth the effort&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to run massive parallel computations efficiently? Check out &lt;a href="https://github.com/basnijholt/adaptive-scheduler" target="_blank" rel="noopener"&gt;Adaptive Scheduler on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #HPC #QuantumComputing #ParallelComputing&lt;/p&gt;&#10;</description></item><item><title>Adaptive 📈</title><link>https://www.nijho.lt/post/advent-of-open-source/16-adaptive/</link><pubDate>Mon, 16 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/16-adaptive/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;During my PhD, I was running quantum transport simulations on our 800-core computational cluster.&#10;Traditional parameter sweeps using dense grids were hitting the cluster&amp;rsquo;s limits - wasting precious compute time on uninteresting regions while potentially missing important features.&#10;Most parameter spaces we explored were &amp;ldquo;boring&amp;rdquo; with occasional interesting regions that required higher sampling density.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Initially, in 2016, I had a collection of one-off adaptive sampling scripts.&#10;Together with Joseph Weston and the support of my PhD advisor Anton Akhmerov, we turned it into a full-fledged Python package &amp;ldquo;&lt;a href="https://github.com/python-adaptive/adaptive" target="_blank" rel="noopener"&gt;Adaptive&lt;/a&gt;&amp;rdquo;.&#10;What began as an internal tool to make our cluster usage more efficient slowly gained traction in the wider scientific computing community.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Multiple adaptive sampling strategies (1D, 2D, ND, averaging stochastic functions)&lt;/li&gt;&#10;&lt;li&gt;Parallel execution support (local, MPI clusters, distributed systems)&lt;/li&gt;&#10;&lt;li&gt;Live plotting in Jupyter notebooks with real-time feedback&lt;/li&gt;&#10;&lt;li&gt;Customizable loss functions for different sampling priorities&lt;/li&gt;&#10;&lt;li&gt;Minimal dependencies&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact--applications"&gt;📊 Impact &amp;amp; Applications &lt;a class="anchor" href="#-impact--applications" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;1164 GitHub stars and thousands of daily downloads&lt;/li&gt;&#10;&lt;li&gt;Cited in 20+ scientific publications&lt;/li&gt;&#10;&lt;li&gt;Dependency of many packages, most notably: Orange Quantum System&amp;rsquo;s Quantify software for controlling real quantum computers!&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Used in cutting-edge research across fields:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Quantum Computing&lt;/strong&gt;: Simulating and controlling superconducting qubits and trapped ion systems&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Quantum Physics&lt;/strong&gt;: Exploring topological quantum systems and novel materials&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Materials Science&lt;/strong&gt;: Studying graphene and other 2D materials&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Engineering&lt;/strong&gt;:&#10;&lt;ul&gt;&#10;&lt;li&gt;Optimizing radar system performance&lt;/li&gt;&#10;&lt;li&gt;Fluid dynamics simulations&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Chemistry&lt;/strong&gt;: Molecular modeling and vibrational spectroscopy&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Most Unexpected Use&lt;/strong&gt;: Found in a paper on predicting energy usage of wartime bungalows!&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This range of applications shows how a tool built for quantum transport simulations can find surprising uses across disciplines, from fundamental physics to everyday buildings.&lt;/p&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Making parallel execution truly scalable while maintaining sampling intelligence&lt;/li&gt;&#10;&lt;li&gt;Creating intuitive visualization tools for complex parameter spaces&lt;/li&gt;&#10;&lt;li&gt;Handling stochastic functions and noisy data&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Building a user base takes active effort - giving talks and engaging online made all the difference&lt;/li&gt;&#10;&lt;li&gt;Good documentation converts interested users into regular contributors&lt;/li&gt;&#10;&lt;li&gt;This was my first open source package and learned all about packaging, testing, and CI/CD&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want smarter parameter space exploration? Check out &lt;a href="https://github.com/python-adaptive/adaptive" target="_blank" rel="noopener"&gt;Adaptive on GitHub&lt;/a&gt; or try our &lt;a href="https://adaptive.readthedocs.io/en/latest/tutorial/tutorial.html" target="_blank" rel="noopener"&gt;interactive tutorial&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #ScientificComputing #ParallelComputing #Programming&lt;/p&gt;&#10;</description></item><item><title>clip-files 📋</title><link>https://www.nijho.lt/post/advent-of-open-source/15-clip-files/</link><pubDate>Sun, 15 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/15-clip-files/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Sometimes the most useful tools are born from daily frustrations.&#10;When working with AI assistants like ChatGPT or Claude, I found myself constantly copying files to share context.&#10;After doing this manually dozens of times, I created a tool to automate it.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While contributing to open source, I frequently like to share entire (or large parts of) codebases with AI assistants for context or code review.&#10;Since the code is open source anyway, there&amp;rsquo;s no reason not to share the full context! But manually copying files, adding their paths for context, and ensuring I didn&amp;rsquo;t exceed token limits was tedious.&#10;Instead of continuing this manual process, I spent an evening creating &lt;a href="https://github.com/basnijholt/clip-files" target="_blank" rel="noopener"&gt;&lt;code&gt;clip-files&lt;/code&gt;&lt;/a&gt; - and have used it multiple times daily ever since.&lt;/p&gt;&#10;&lt;p&gt;In fact, I&amp;rsquo;m using it right now! The command (shown below) lets me share this entire advent calendar series with AI assistants for proofreading.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;clip-files --initial-file ai.md --files introduction.md day*.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Uses &lt;code&gt;tiktoken&lt;/code&gt; for accurate GPT token counting&lt;/li&gt;&#10;&lt;li&gt;Supports both directory traversal and specific file selection&lt;/li&gt;&#10;&lt;li&gt;Automatically generates a file index for reference&lt;/li&gt;&#10;&lt;li&gt;Allows custom initial instructions via file&lt;/li&gt;&#10;&lt;li&gt;Preserves file paths in output for context&lt;/li&gt;&#10;&lt;li&gt;Clean, typed, and documented codebase&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While this tool has no GitHub stars (I haven&amp;rsquo;t promoted it), its real impact is personal - I use it multiple times daily for:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Sharing entire open source codebases with AI assistants&lt;/li&gt;&#10;&lt;li&gt;Getting comprehensive code reviews&lt;/li&gt;&#10;&lt;li&gt;Discussing bug fixes with full context&lt;/li&gt;&#10;&lt;li&gt;Documentation improvements&lt;/li&gt;&#10;&lt;li&gt;Open source contributions&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The tool&amp;rsquo;s real value isn&amp;rsquo;t in its complexity (it&amp;rsquo;s just ~250 lines of code) but in how it removes friction from open source development.&#10;When working with open code, we might as well share all the context we can - and this tool makes that effortless.&lt;/p&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Maintaining consistent formatting across different input methods&lt;/li&gt;&#10;&lt;li&gt;Calculating accurate token counts for different components&lt;/li&gt;&#10;&lt;li&gt;Providing meaningful error messages for invalid inputs&lt;/li&gt;&#10;&lt;li&gt;Structuring output to be AI-assistant friendly&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;When working with open source code, sharing full context is valuable&lt;/li&gt;&#10;&lt;li&gt;Simple command-line tools can significantly improve daily workflows&lt;/li&gt;&#10;&lt;li&gt;Consistent formatting matters when working with AI assistants&lt;/li&gt;&#10;&lt;li&gt;Sometimes a 250-line tool can save hours of manual work&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to make sharing code with AI assistants easier? Check out &lt;a href="https://github.com/basnijholt/clip-files" target="_blank" rel="noopener"&gt;clip-files on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #DevTools #AI #Programming&lt;/p&gt;&#10;</description></item><item><title>PhD Thesis Cover Design 🎨</title><link>https://www.nijho.lt/post/advent-of-open-source/14-phd-thesis-cover-design/</link><pubDate>Sat, 14 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/14-phd-thesis-cover-design/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;When most people think of open source, they think of software. But what about open-sourcing your PhD thesis and creating a generative art system for its cover?&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;With my PhD defense approaching and needing around 100 copies of my thesis, I faced a design challenge. As someone more comfortable with Python than Photoshop, I turned what started as a simple visualization idea into a full-blown generative art project with machine learning-based clustering.&lt;/p&gt;&#10;&lt;figure id="figure-multiple-printed-copies-of-the-thesis"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/14-phd-thesis-cover-design/image_2_hu_8cd2dfb49d0239e5.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/14-phd-thesis-cover-design/image_2_hu_91ed23b7c6d41771.webp 400w, https://www.nijho.lt/post/advent-of-open-source/14-phd-thesis-cover-design/image_2_hu_8cd2dfb49d0239e5.webp 760w, https://www.nijho.lt/post/advent-of-open-source/14-phd-thesis-cover-design/image_2_hu_1ad1cec7a7457f36.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="616" alt="A photograph of multiple printed copies of Bas&amp;#39;s PhD thesis laid out on a table outdoors. Each copy has a unique, colorful, generated cover design. Potted plants are visible in the background." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Multiple printed copies of the thesis&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Generated 3,671 unique covers from quantum physics simulation data&lt;/li&gt;&#10;&lt;li&gt;Used VGG16 neural network to extract features from covers&lt;/li&gt;&#10;&lt;li&gt;Implemented K-means clustering to group similar designs&lt;/li&gt;&#10;&lt;li&gt;Created interactive IPywidgets for design selection&lt;/li&gt;&#10;&lt;li&gt;Built a Binder app for real-time color scheme adjustment&lt;/li&gt;&#10;&lt;li&gt;Added unique edition numbers (1/120, 2/120, etc.)&lt;/li&gt;&#10;&lt;li&gt;Made both thesis and cover design code fully open source&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The project combines:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Adaptive sampling for physics simulations&lt;/li&gt;&#10;&lt;li&gt;Neural networks for image feature extraction&lt;/li&gt;&#10;&lt;li&gt;Interactive visualization tools&lt;/li&gt;&#10;&lt;li&gt;Parallel processing for batch generation&lt;/li&gt;&#10;&lt;li&gt;Version control for scientific writing&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Thesis repository: 14 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;Cover design repository: 36 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;Real-world adoption: &lt;a href="https://repository.tudelft.nl/record/uuid:e88a1897-0033-47e3-8b4f-84fd9cd5eec0" target="_blank" rel="noopener"&gt;Dr. Tomas Pippia from TU Delft&lt;/a&gt; used the code to create his own thesis cover&lt;/li&gt;&#10;&lt;li&gt;~100 unique printed copies, each with its own generated cover and edition number&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Managing 3,671 unique PDF files and their conversions&lt;/li&gt;&#10;&lt;li&gt;Creating a clustering system for visually similar designs&lt;/li&gt;&#10;&lt;li&gt;Building interactive tools for non-technical collaborators&lt;/li&gt;&#10;&lt;li&gt;Ensuring consistent print quality across designs&lt;/li&gt;&#10;&lt;li&gt;Balancing artistic appeal with scientific relevance&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Open source isn&amp;rsquo;t just about code - it&amp;rsquo;s about sharing knowledge&lt;/li&gt;&#10;&lt;li&gt;Python can be a powerful tool for creative projects&lt;/li&gt;&#10;&lt;li&gt;Machine learning can help solve unexpected problems&lt;/li&gt;&#10;&lt;li&gt;Good visualization requires both technical skill and artistic sense&lt;/li&gt;&#10;&lt;li&gt;Interactive tools make collaboration with non-programmers easier&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="-fun-fact"&gt;📚 Fun Fact &lt;a class="anchor" href="#-fun-fact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Due to COVID restrictions, I defended my thesis without any guests present. The silver lining? I still have about 100 beautifully unique copies at home, each with a different generated cover and edition number. If you&amp;rsquo;d like a physical copy of this piece of quantum physics meets generative art, just let me know!&lt;/p&gt;&#10;&lt;p&gt;Want to create your own thesis cover or see how I wrote mine? Check out the &lt;a href="https://github.com/basnijholt/thesis-cover" target="_blank" rel="noopener"&gt;thesis cover project&lt;/a&gt; and &lt;a href="https://github.com/basnijholt/thesis" target="_blank" rel="noopener"&gt;thesis source&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #PhD #Python #MachineLearning #DataVisualization&lt;/p&gt;&#10;</description></item><item><title>UniDep 🧬</title><link>https://www.nijho.lt/post/advent-of-open-source/13-unidep/</link><pubDate>Fri, 13 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/13-unidep/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Ever tried to set up a Python project that needs both pip and conda packages? Then you know the pain of maintaining multiple requirement files. &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;UniDep&lt;/a&gt; was born from years of frustration with this exact problem.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While working on large monorepos with hundreds of dependencies, I kept running into the same issue: maintaining separate files for conda (&lt;code&gt;environment.yml&lt;/code&gt;), pip (&lt;code&gt;requirements.txt&lt;/code&gt;), and Python packaging (&lt;code&gt;pyproject.toml&lt;/code&gt;). Coming from languages like Rust, where package management is a joy, Python&amp;rsquo;s fragmented ecosystem can feel overwhelming, especially to newcomers (obligatory &lt;a href="https://xkcd.com/1987/" target="_blank" rel="noopener"&gt;xkcd.com/1987&lt;/a&gt;).&lt;/p&gt;&#10;&lt;p&gt;What started as a simple tool to avoid duplicate dependency definitions turned into something much more powerful. In enterprise environments, it&amp;rsquo;s common to find monorepos with over 1000 dependencies spread across dozens of packages. Setting up these environments traditionally involves following lengthy setup guides, installing packages in the correct order, and hoping nothing breaks.&lt;/p&gt;&#10;&lt;p&gt;Then came the &amp;ldquo;aha&amp;rdquo; moment - what if we could install everything with a single command? After implementing UniDep, those complex setup guides became a one-liner: &lt;code&gt;unidep install-all -e&lt;/code&gt;. It installs all dependencies and sets up every package in the monorepo in editable mode. What used to take hours now takes minutes.&lt;/p&gt;&#10;&lt;figure id="figure-the-difficulty-of-managing-pip-and-conda"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/13-unidep/meme_hu_1916102bc051ce0e.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/13-unidep/meme_hu_992ea341768024aa.webp 400w, https://www.nijho.lt/post/advent-of-open-source/13-unidep/meme_hu_1916102bc051ce0e.webp 760w, https://www.nijho.lt/post/advent-of-open-source/13-unidep/meme_hu_283eb791a4b7e11e.webp 980w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="447" alt="Meme with the text &amp;#39;ONE DOES NOT SIMPLY MANAGE BOTH PIP AND CONDA DEPENDENCIES&amp;#39; featuring Sean Bean as Boromir from The Lord of the Rings." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;The difficulty of managing pip and conda&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Single source of truth for all dependencies&lt;/li&gt;&#10;&lt;li&gt;Supports both &lt;code&gt;requirements.yaml&lt;/code&gt; and &lt;code&gt;pyproject.toml&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;Handles platform-specific dependencies&lt;/li&gt;&#10;&lt;li&gt;Generates conda-lock files both for entire monorepos and fully consistent lock files for its individual packages&lt;/li&gt;&#10;&lt;li&gt;Works with pip, conda, and mamba&lt;/li&gt;&#10;&lt;li&gt;Monorepo-friendly with local dependency support&lt;/li&gt;&#10;&lt;li&gt;Integrates with setuptools and hatchling&lt;/li&gt;&#10;&lt;li&gt;≥99% test coverage, fully-typed&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;221 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;Featured on &lt;a href="https://www.youtube.com/live/PRaTs3PnJvI?si=UrVozo81Pj8WcyXh&amp;amp;t=489" target="_blank" rel="noopener"&gt;Python Bytes podcast&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Saves countless hours of dependency management&lt;/li&gt;&#10;&lt;li&gt;Makes complex project setups accessible&lt;/li&gt;&#10;&lt;li&gt;Actively maintained with 70 releases&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Different package naming between pip and conda&lt;/li&gt;&#10;&lt;li&gt;Platform-specific dependencies&lt;/li&gt;&#10;&lt;li&gt;Cross-platform compatibility (Linux, Windows, MacOS)&lt;/li&gt;&#10;&lt;li&gt;Supporting pip-only workflows&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Good developer experience is worth the effort&lt;/li&gt;&#10;&lt;li&gt;Sometimes you need to build a bridge between existing tools&lt;/li&gt;&#10;&lt;li&gt;The best tools make hard things easy&lt;/li&gt;&#10;&lt;li&gt;Dependency resolution is hard but can be simple for users&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="-future-plans"&gt;🔮 Future Plans &lt;a class="anchor" href="#-future-plans" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Adding Pixi support - allowing seamless migration to Pixi without changing config files&lt;/li&gt;&#10;&lt;li&gt;Eventually, Pixi might make UniDep obsolete (and that&amp;rsquo;s okay!)&lt;/li&gt;&#10;&lt;li&gt;For now, UniDep bridges the gap between pip and conda, making life easier for developers&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Want simpler dependency management? Check out &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;UniDep on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #DevTools #Programming #PackageManagement #Conda&lt;/p&gt;&#10;</description></item><item><title>instacron 📸</title><link>https://www.nijho.lt/post/advent-of-open-source/12-instacron/</link><pubDate>Thu, 12 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/12-instacron/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Ever found yourself with beautiful photos but too lazy to post them? Instead of changing my habits, I did what any developer would do: spent way more time automating the process than it would have taken to post manually.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Back in 2017, I had hundreds of landscape photos from traveling through South America that I wanted to share on Instagram. The solution? Write a Python script that would randomly select photos and post them automatically, complete with philosophical quotes randomly selected from Bukowski, Einstein, or Dostoevsky because&amp;hellip; it seemed to be the default of all &amp;ldquo;influencers&amp;rdquo; at the time. Nothing says &amp;ldquo;authentic travel experience&amp;rdquo; like an auto-generated Bukowski quote about drinking next to your serene mountain photo!&lt;/p&gt;&#10;&lt;figure id="figure-instacron-example-post"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_2_hu_b99c73d57edbea5b.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_2_hu_5a3939660236b043.webp 400w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_2_hu_b99c73d57edbea5b.webp 760w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_2_hu_749d311a92e20dbc.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="433" alt="Screenshot of an Instagram post with a person standing in a stone archway overlooking a sunset, captioned &amp;#39;Taken in United States, Santa Barbara&amp;#39; with hashtags related to nature and travel." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Instacron Example Post&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;figure id="figure-instacron-example-post"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_3_hu_847939577f1e3dd1.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_3_hu_a10eeae7bee69925.webp 400w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_3_hu_847939577f1e3dd1.webp 760w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_3_hu_12c1ed0c7f55a937.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="558" alt="Screenshot of an Instagram post of a cave with a river flowing through it, captioned &amp;#39;Taken in Thailand, some cave&amp;#39; with hashtags related to travel and adventure." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Instacron Example Post&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;figure id="figure-instacron-example-post"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_4_hu_8832527ce6759925.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_4_hu_5430615296d94efb.webp 400w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_4_hu_8832527ce6759925.webp 760w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_4_hu_be87e46e6482de14.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="473" alt="Screenshot of an Instagram post showing a misty mountain landscape at sunrise, captioned &amp;#39;Taken in Thailand-Laos border&amp;#39; with hashtags related to nature and travel." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Instacron Example Post&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;figure id="figure-instacron-example-post"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_5_hu_10c2294abc4f0ed4.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_5_hu_8e63ad58ac7e0b76.webp 400w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_5_hu_10c2294abc4f0ed4.webp 760w, https://www.nijho.lt/post/advent-of-open-source/12-instacron/image_5_hu_226dfc0f12041227.webp 1200w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="480" alt="Screenshot of an Instagram post of a mountain range with a green field in the foreground, captioned &amp;#39;Taken in Lago Argentino, SC, Argentina&amp;#39; with hashtags related to nature and travel." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Instacron Example Post&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Randomly selects photos from a directory&lt;/li&gt;&#10;&lt;li&gt;Adds location data from EXIF metadata&lt;/li&gt;&#10;&lt;li&gt;Generates captions with random philosophical quotes&lt;/li&gt;&#10;&lt;li&gt;Adds two random emojis to match the &amp;ldquo;mood&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;Automatically generates relevant hashtags&lt;/li&gt;&#10;&lt;li&gt;Tracks posting frequency&lt;/li&gt;&#10;&lt;li&gt;Uses cron for scheduled posting&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;21 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;4143 Instagram followers&lt;/li&gt;&#10;&lt;li&gt;Successfully posted hundreds of photos&lt;/li&gt;&#10;&lt;li&gt;Generated some concerned DMs (more on that below!)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Instagram&amp;rsquo;s ever-changing API&lt;/li&gt;&#10;&lt;li&gt;Handling rate limits and restrictions&lt;/li&gt;&#10;&lt;li&gt;Ensuring photos weren&amp;rsquo;t posted multiple times&lt;/li&gt;&#10;&lt;li&gt;Making captions interesting without human input&lt;/li&gt;&#10;&lt;li&gt;The delicate art of random emoji selection&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-funny-story"&gt;😅 Funny Story &lt;a class="anchor" href="#-funny-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The random quote and emoji selector once created an unexpectedly dramatic post. Picture this: a beautiful landscape photo paired with a deeply philosophical quote about solitude, and topped with a broken heart emoji (💔). Within hours, I started receiving concerned DMs from friends asking if everything was okay and whether my relationship had ended!&lt;/p&gt;&#10;&lt;p&gt;I had to explain that no, I wasn&amp;rsquo;t going through a breakup - my bot just had a flair for the dramatic! These occasional mismatches made the whole thing more entertaining.&lt;/p&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Sometimes the best projects come from pure laziness&lt;/li&gt;&#10;&lt;li&gt;Automation doesn&amp;rsquo;t always save time, but it&amp;rsquo;s fun&lt;/li&gt;&#10;&lt;li&gt;Never underestimate the power of a misplaced emoji to trigger concerned messages&lt;/li&gt;&#10;&lt;li&gt;Some followers actually care about you (even if they&amp;rsquo;re worried about your bot-generated content)&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;While I haven&amp;rsquo;t used this project in 6 years (Instagram&amp;rsquo;s API changes eventually won), it remains a testament to the lengths we&amp;rsquo;ll go to avoid repetitive tasks. Check out &lt;a href="https://github.com/basnijholt/instacron" target="_blank" rel="noopener"&gt;instacron on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #Automation #Photography #Instagram&lt;/p&gt;&#10;</description></item><item><title>conda-forge Contributions 🐍</title><link>https://www.nijho.lt/post/advent-of-open-source/11-conda-forge-contributions/</link><pubDate>Wed, 11 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/11-conda-forge-contributions/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Unlike other projects in this advent calendar, I want to celebrate being part of something much bigger: the &lt;a href="https://conda-forge.org/" target="_blank" rel="noopener"&gt;conda-forge&lt;/a&gt; community, where thousands of volunteers maintain the packages that power scientific Python.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I started with conda-forge in 2016 during my PhD work with Kwant, a quantum transport simulator. Installing it was a nightmare - it required system-level numerical libraries like MUMPS and Scotch which required a special config file to link. When I discovered conda-forge and its mission to make scientific software installation painless, I knew this was the solution!&lt;/p&gt;&#10;&lt;figure id="figure-the-humble-work-of-conda-forge-maintainers"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/11-conda-forge-contributions/meme_hu_227391ecf0bd252.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/11-conda-forge-contributions/meme_hu_9b170f53ebe4dc1b.webp 400w, https://www.nijho.lt/post/advent-of-open-source/11-conda-forge-contributions/meme_hu_227391ecf0bd252.webp 760w, https://www.nijho.lt/post/advent-of-open-source/11-conda-forge-contributions/meme_hu_95b0c28d6df1a41a.webp 978w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="586" alt="Meme showing a farmer standing in a field, with the text &amp;#39;MAINTAINING CONDA-FORGE PACKAGES FOR THE SCIENTIFIC COMMUNITY&amp;#39; above and &amp;#39;It ain&amp;#39;t much, but it&amp;#39;s honest work&amp;#39; below." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;The humble work of conda-forge maintainers&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Over the past 8 years, I&amp;rsquo;ve maintained over 40 build recipes, including:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Major scientific packages like &lt;a href="https://vtk.org/" target="_blank" rel="noopener"&gt;VTK&lt;/a&gt;, &lt;a href="https://mumps-solver.org/" target="_blank" rel="noopener"&gt;MUMPS&lt;/a&gt;, and &lt;a href="https://www.labri.fr/perso/pelegrin/scotch/" target="_blank" rel="noopener"&gt;Scotch&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Quantum physics tools like &lt;a href="https://quantumai.google/qsim" target="_blank" rel="noopener"&gt;Qsim&lt;/a&gt;, &lt;a href="https://github.com/microsoft/Qcodes" target="_blank" rel="noopener"&gt;Qcodes&lt;/a&gt;, and &lt;a href="https://kwant-project.org/" target="_blank" rel="noopener"&gt;Kwant&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Visualization libraries like &lt;a href="https://holoviews.org/" target="_blank" rel="noopener"&gt;HoloViews&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Plus all the packages I&amp;rsquo;ve authored myself&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The real magic of conda-forge is its infrastructure:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Automated builds across Linux, macOS, and Windows&lt;/li&gt;&#10;&lt;li&gt;Strict dependency version management&lt;/li&gt;&#10;&lt;li&gt;Comprehensive CI/CD pipelines&lt;/li&gt;&#10;&lt;li&gt;Community-driven quality control&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;What makes conda-forge truly special is how initial contributions blossom into collaborative efforts. I&amp;rsquo;ve experienced this firsthand: after creating the initial MUMPS feedstock, 25 other contributors joined in, each bringing their unique expertise.&lt;/p&gt;&#10;&lt;p&gt;A recent experience also illustrates this: I added CUDA support to both &lt;a href="https://github.com/conda-forge/qsimcirq-feedstock/pull/3" target="_blank" rel="noopener"&gt;Google&amp;rsquo;s Qsim&lt;/a&gt; and &lt;a href="https://github.com/conda-forge/qiskit-aer-feedstock/pull/19" target="_blank" rel="noopener"&gt;IBM&amp;rsquo;s Cirq&lt;/a&gt; packages. Shortly after, &lt;a href="https://github.com/leofang" target="_blank" rel="noopener"&gt;Leo Fang&lt;/a&gt;, an engineer from NVIDIA who knows CUDA far better than I do, stepped up and made significant improvements. This is the beauty of open source - experts naturally gravitate to where they can make the biggest impact.&lt;/p&gt;&#10;&lt;p&gt;This collaborative approach solves a problem I&amp;rsquo;ve encountered at several companies: the tendency to maintain complicated build systems internally with custom hacks. Instead of each organization reinventing the wheel, conda-forge provides a shared platform where:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Maintenance burden is distributed&lt;/li&gt;&#10;&lt;li&gt;Expert knowledge flows freely&lt;/li&gt;&#10;&lt;li&gt;Build practices are standardized&lt;/li&gt;&#10;&lt;li&gt;Infrastructure is public and reusable&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Keeping up with upstream changes&lt;/li&gt;&#10;&lt;li&gt;Managing complex dependency trees&lt;/li&gt;&#10;&lt;li&gt;Cross-platform compatibility&lt;/li&gt;&#10;&lt;li&gt;Coordinating with upstream maintainers&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Community effort beats solo work for infrastructure&lt;/li&gt;&#10;&lt;li&gt;Automation is crucial for reliability&lt;/li&gt;&#10;&lt;li&gt;Small contributions compound over time&lt;/li&gt;&#10;&lt;li&gt;Being part of something bigger is rewarding&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to contribute to scientific Python? Check out &lt;a href="https://conda-forge.org/" target="_blank" rel="noopener"&gt;conda-forge&lt;/a&gt; and the &lt;a href="https://github.com/conda-forge/staged-recipes" target="_blank" rel="noopener"&gt;staged-recipes repository&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #Scientific #Programming #Community&lt;/p&gt;&#10;</description></item><item><title>Home Assistant Config 🏠</title><link>https://www.nijho.lt/post/advent-of-open-source/10-home-assistant-config/</link><pubDate>Tue, 10 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/10-home-assistant-config/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Funny how things work out - initially, I didn&amp;rsquo;t care for home automation at all. But when in 2020 I discovered Home Assistant was written in Python, my favorite programming language, I suddenly found a new hobby. Over the years, I&amp;rsquo;ve built a comprehensive setup with over 137 automations (and counting!). These automations are triggered by everything from simple motion sensors to complex patterns of conditions and sensor states, creating a home that responds intelligently to our needs.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;What started with basic light controls evolved into a complete home automation system. In the beginning, my wife was continuously annoyed by lights turning on at the wrong moment or not at all. However, as I debugged and refined the system, things started working exactly as expected, and she began to really appreciate it. When we moved from The Netherlands to the US in 2022 (and two more times since), we felt like we were back in the stone age having to manually control lights. The annoyance had shifted from buggy automations to not having Home Assistant fully installed yet!&lt;/p&gt;&#10;&lt;p&gt;Unfortunately, I&amp;rsquo;ve been less active lately with my Home Assistant setup because so many other projects draw my attention. One of these &amp;ldquo;projects&amp;rdquo; is my 1.5-year-old twin boys, who now have their own room with specialized automations, including 100% local cameras with ML pattern detection for monitoring without compromising privacy.&lt;/p&gt;&#10;&lt;figure id="figure-the-evolution-of-home-automation"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/10-home-assistant-config/meme_hu_6700885751a5eed.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/10-home-assistant-config/meme_hu_a2f1eaea094dec73.webp 285w, https://www.nijho.lt/post/advent-of-open-source/10-home-assistant-config/meme_hu_6700885751a5eed.webp 542w, https://www.nijho.lt/post/advent-of-open-source/10-home-assistant-config/meme_hu_2a8639bb88253b09.webp 856w" sizes="auto, (max-width: 760px) 100vw, 720px" width="542" height="760" alt="Meme with four panels, each showing a brain with increasing complexity. The panels are labeled: &amp;#39;MANUAL LIGHT SWITCHES&amp;#39;, &amp;#39;SMART BULBS&amp;#39;, &amp;#39;MOTION SENSORS&amp;#39;, and &amp;#39;137 AUTOMATIONS RUNNING YOUR ENTIRE HOME&amp;#39;." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;The evolution of home automation&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;137 documented automations with auto-generated documentation&lt;/li&gt;&#10;&lt;li&gt;Presence detection in every room&lt;/li&gt;&#10;&lt;li&gt;Adaptive lighting throughout the house&lt;/li&gt;&#10;&lt;li&gt;Privacy-focused baby monitoring&lt;/li&gt;&#10;&lt;li&gt;Fully local processing&lt;/li&gt;&#10;&lt;li&gt;2,432 commits of continuous improvement&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;1,747 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;185 forks&lt;/li&gt;&#10;&lt;li&gt;Used as reference by countless Home Assistant enthusiasts&lt;/li&gt;&#10;&lt;li&gt;Featured in Home Assistant community&lt;/li&gt;&#10;&lt;li&gt;Spawned several spin-off projects (like &lt;a href="../02-adaptive-lighting"&gt;Adaptive Lighting&lt;/a&gt; from Day 2!)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Start small, expand gradually&lt;/li&gt;&#10;&lt;li&gt;Privacy and local processing matter&lt;/li&gt;&#10;&lt;li&gt;Automated documentation stays accurate&lt;/li&gt;&#10;&lt;li&gt;Home automation is never &amp;ldquo;finished&amp;rdquo;&lt;/li&gt;&#10;&lt;li&gt;Sometimes life (like twins!) brings new automation challenges&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to explore my home automation setup? Check out &lt;a href="https://github.com/basnijholt/home-assistant-config" target="_blank" rel="noopener"&gt;the full config on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #HomeAssistant #SmartHome #IoT #Python&lt;/p&gt;&#10;</description></item><item><title>yaml2bib 📚</title><link>https://www.nijho.lt/post/advent-of-open-source/09-yaml2bib/</link><pubDate>Mon, 09 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/09-yaml2bib/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Every academic writing in LaTeX knows the pain: managing BibTeX files with hundreds of citations, each potentially formatted slightly differently, with inconsistent journal abbreviations, and duplicated entries across chapters. While procrastinating on writing my PhD thesis, I became obsessed with solving this universal academic headache.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When writing a PhD thesis with multiple chapters that share citations, maintaining consistency is crucial but tedious. Different chapters might cite the same paper but with slightly different BibTeX entries - one using the full journal name, another using abbreviations, or worse, having slightly different titles or author lists. Rather than manually standardizing ~350 citations, I created yaml2bib: give it a list of DOIs in YAML format, and it generates a perfect BibTeX file with consistent journal abbreviations and formatting.&lt;/p&gt;&#10;&lt;figure id="figure-the-pain-of-manual-bibtex-management"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/09-yaml2bib/meme_hu_aac0680d796f5d9.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/09-yaml2bib/meme_hu_f547b35ecc31869c.webp 400w, https://www.nijho.lt/post/advent-of-open-source/09-yaml2bib/meme_hu_aac0680d796f5d9.webp 760w, https://www.nijho.lt/post/advent-of-open-source/09-yaml2bib/meme_hu_5f55d375b7929c53.webp 976w" sizes="auto, (max-width: 760px) 100vw, 720px" width="760" height="371" alt="Meme depicting a cartoon dog sitting calmly in a room engulfed in flames, saying &amp;#39;This is fine.&amp;#39; The dog is labeled &amp;#39;Managing 300&amp;#43; BibTeX entries manually&amp;#39;." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;The pain of manual BibTeX management&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Fetches citation data from CrossRef using DOIs&lt;/li&gt;&#10;&lt;li&gt;Ensures consistent journal name abbreviations&lt;/li&gt;&#10;&lt;li&gt;Caches API responses to avoid repeated queries&lt;/li&gt;&#10;&lt;li&gt;Supports custom text replacements&lt;/li&gt;&#10;&lt;li&gt;Combines multiple YAML sources&lt;/li&gt;&#10;&lt;li&gt;Works as both CLI tool and Python library&lt;/li&gt;&#10;&lt;li&gt;Zero manual citation formatting needed&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While this tool has only 11 GitHub stars, it saved me countless hours during thesis writing and helped ensure professional consistency in my citations. It&amp;rsquo;s particularly useful for:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Large academic documents with many citations&lt;/li&gt;&#10;&lt;li&gt;Multi-chapter works sharing references&lt;/li&gt;&#10;&lt;li&gt;Collaborative papers needing citation standardization&lt;/li&gt;&#10;&lt;li&gt;Anyone tired of manually formatting BibTeX entries&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Procrastination can be productive if channeled right (story of my life&amp;hellip;)&lt;/li&gt;&#10;&lt;li&gt;A single source of truth prevents inconsistencies&lt;/li&gt;&#10;&lt;li&gt;Good tools make you care about details you&amp;rsquo;d otherwise skip&lt;/li&gt;&#10;&lt;li&gt;Every LaTeX user has fought with BibTeX at some point&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want perfectly consistent citations? Check out &lt;a href="https://github.com/basnijholt/yaml2bib" target="_blank" rel="noopener"&gt;yaml2bib on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #Academia #LaTeX #Research&lt;/p&gt;&#10;</description></item><item><title>Home Assistant Stream Deck YAML 🎮</title><link>https://www.nijho.lt/post/advent-of-open-source/08-home-assistant-stream-deck-yaml/</link><pubDate>Sun, 08 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/08-home-assistant-stream-deck-yaml/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;What started as a personal hack to control my home (temperature, lights, TV, music) with a gifted Stream Deck evolved into something bigger. This project became my first real exploration of AI-assisted development, showing me how much faster I could work with AI as a tool. With 250+ stars and 9 contributors adding significant features, it&amp;rsquo;s grown beyond my initial vision.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While the Stream Deck&amp;rsquo;s official software is great for streamers, it doesn&amp;rsquo;t support Linux and its Home Assistant integration is not very programmable. After creating a simple solution using &lt;a href="https://github.com/abcminiuser/python-elgato-streamdeck" target="_blank" rel="noopener"&gt;&lt;code&gt;python-elgato-streamdeck&lt;/code&gt;&lt;/a&gt;, I realized others might be interested. Using AI as a development partner changed my workflow - it helped write tests, reviewed code, and generated use cases I hadn&amp;rsquo;t thought of. What really surprised me was asking AI to brainstorm example configurations - it generated over 30 creative use cases with complete code examples, which I then turned into unit tests. These tests now automatically generate the documentation using &lt;a href="https://github.com/basnijholt/markdown-code-runner" target="_blank" rel="noopener"&gt;&lt;code&gt;markdown-code-runner&lt;/code&gt; &lt;/a&gt;(&lt;a href="../05-markdown-code-runner"&gt;from Day 5&lt;/a&gt;).&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;YAML configuration with Jinja2 templating&lt;/li&gt;&#10;&lt;li&gt;Supports all Stream Deck models, including the Plus with dials&lt;/li&gt;&#10;&lt;li&gt;Cross-platform (Linux, macOS, Windows)&lt;/li&gt;&#10;&lt;li&gt;30+ example configurations (AI-generated, then human-refined)&lt;/li&gt;&#10;&lt;li&gt;Documentation auto-generated from code and unit tests&lt;/li&gt;&#10;&lt;li&gt;Available as a Home Assistant add-on&lt;/li&gt;&#10;&lt;li&gt;Community-contributed features like touchscreen support and dial controls&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;AI can significantly speed up development&lt;/li&gt;&#10;&lt;li&gt;Faster development enables broader project scope&lt;/li&gt;&#10;&lt;li&gt;Good examples make the best documentation&lt;/li&gt;&#10;&lt;li&gt;Tools building upon tools multiply productivity&lt;/li&gt;&#10;&lt;li&gt;Community contributions can take a project in exciting new directions&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to control Home Assistant with a Stream Deck? Check out &lt;a href="https://github.com/basnijholt/home-assistant-streamdeck-yaml" target="_blank" rel="noopener"&gt;the project on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #HomeAssistant #SmartHome #Python #AI&lt;/p&gt;&#10;</description></item><item><title>numthreads 🧵</title><link>https://www.nijho.lt/post/advent-of-open-source/07-numthreads/</link><pubDate>Sat, 07 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/07-numthreads/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Sometimes the smallest tools solve the most persistent problems.&#10;Today&amp;rsquo;s project is about taming automatic parallelization in scientific computing - a deceptively simple challenge that has cost countless CPU hours.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While working on high-performance computing clusters, I frequently encountered a counterintuitive problem: code running slower on multiple CPU cores than on a single one.&#10;Many scientific libraries (NumPy, SciPy) automatically parallelize operations, which sounds great but can actually harm performance when you&amp;rsquo;re already parallelizing at a higher level.&#10;After repeatedly explaining to colleagues why they needed to set various environment variables to disable this behavior, I created &lt;a href="https://github.com/basnijholt/numthreads" target="_blank" rel="noopener"&gt;&lt;code&gt;numthreads&lt;/code&gt;&lt;/a&gt; to solve it once and for all.&lt;/p&gt;&#10;&lt;figure id="figure-overenthusiastic-threading"&gt;&lt;img src="https://www.nijho.lt/post/advent-of-open-source/07-numthreads/meme_hu_4d36ab41a66a48cb.webp" srcset="https://www.nijho.lt/post/advent-of-open-source/07-numthreads/meme_hu_8178332437a0cb36.webp 297w, https://www.nijho.lt/post/advent-of-open-source/07-numthreads/meme_hu_4d36ab41a66a48cb.webp 565w, https://www.nijho.lt/post/advent-of-open-source/07-numthreads/meme_hu_f55150e31c7bd3b9.webp 891w" sizes="auto, (max-width: 760px) 100vw, 720px" width="565" height="760" alt="Meme showing a person labeled &amp;#39;YOUR CODE&amp;#39; about to be hugged by a pink creature labeled &amp;#39;NUMTHREADS.SET(1)&amp;#39;, representing setting the number of threads to 1. In the background, a yellow circle labeled &amp;#39;CPU PERFORMANCE&amp;#39; is visible." loading="lazy" data-zoomable&gt;&lt;figcaption&gt;Overenthusiastic threading&lt;/figcaption&gt;&lt;/figure&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Controls thread count for major numerical libraries:&#10;&lt;ul&gt;&#10;&lt;li&gt;OpenBLAS&lt;/li&gt;&#10;&lt;li&gt;Intel&amp;rsquo;s Math Kernel Library (MKL)&lt;/li&gt;&#10;&lt;li&gt;OpenMP&lt;/li&gt;&#10;&lt;li&gt;NumExpr&lt;/li&gt;&#10;&lt;li&gt;Accelerate&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Zero dependencies&lt;/li&gt;&#10;&lt;li&gt;Tiny (≤7KB) yet solves a real problem&lt;/li&gt;&#10;&lt;li&gt;Both CLI and Python API&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Over my scientific computing career, I&amp;rsquo;ve encountered this threading issue dozens of times. Since creating this package a year ago, I&amp;rsquo;ve helped at least 5 different researchers speed up their workflows, sometimes by orders of magnitude. The beauty lies in its simplicity - the fix is often as simple as:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="k"&gt;$(&lt;/span&gt;numthreads 1&lt;span class="k"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;The simplest solutions are often the best&lt;/li&gt;&#10;&lt;li&gt;More cores doesn&amp;rsquo;t always mean faster code&lt;/li&gt;&#10;&lt;li&gt;If you find yourself repeating the same instructions, write a tool&lt;/li&gt;&#10;&lt;li&gt;Even a 7KB package can save hours of computing time&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want better control over your numerical computations? Check out &lt;a href="https://github.com/basnijholt/numthreads" target="_blank" rel="noopener"&gt;numthreads on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #HPC #ScientificComputing #Programming&lt;/p&gt;&#10;</description></item><item><title>calver-auto-release</title><link>https://www.nijho.lt/project/calver-auto-release/</link><pubDate>Fri, 06 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/calver-auto-release/</guid><description/></item><item><title>Open Science Publications 🔬</title><link>https://www.nijho.lt/post/advent-of-open-source/06-open-science-publications/</link><pubDate>Fri, 06 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/06-open-science-publications/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Today&amp;rsquo;s post is about something I have strong opinions on: making scientific research reproducible. While many researchers talk about open science, actually making your work reproducible for anyone requires significant effort.&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;During my Ph.D. and subsequent research, I noticed a frustrating pattern in scientific publications: claims of &amp;ldquo;code available upon request&amp;rdquo; that often led nowhere, or code snippets that wouldn&amp;rsquo;t run without substantial modification. I made an effort that my publications would be different – each includes complete, runnable code that reproduces every figure and result.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Multiple repositories covering quantum physics experiments and simulations:&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/orbitalfield" target="_blank" rel="noopener"&gt;orbitalfield&lt;/a&gt; - &amp;ldquo;Orbital effect of magnetic field on the Majorana phase diagram&amp;rdquo; (94 citations)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/supercurrent-majorana-nanowire" target="_blank" rel="noopener"&gt;supercurrent-majorana-nanowire&lt;/a&gt; - &amp;ldquo;Supercurrent interference in few-mode nanowire Josephson junctions&amp;rdquo; (78 citations)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/zigzag-majoranas" target="_blank" rel="noopener"&gt;zigzag-majoranas&lt;/a&gt; - &amp;ldquo;Enhanced proximity effect in zigzag-shaped Majorana Josephson junctions&amp;rdquo; (56 citations)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/microsoft/azure-quantum-tgp" target="_blank" rel="noopener"&gt;azure-quantum-tgp&lt;/a&gt; - &amp;ldquo;Protocol to identify a topological superconducting phase in a three-terminal device&amp;rdquo; (52 citations)&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/spin-orbit-nanowires" target="_blank" rel="noopener"&gt;spin-orbit-nanowires&lt;/a&gt; - &amp;ldquo;Spin-orbit protection of induced superconductivity in Majorana nanowires&amp;rdquo; (81 citations)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-evolution-of-best-practices"&gt;🔄 Evolution of Best Practices &lt;a class="anchor" href="#-evolution-of-best-practices" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;What I consider best practices has evolved over time. Today, I would:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Use &lt;a href="https://pixi.sh/" target="_blank" rel="noopener"&gt;Pixi&lt;/a&gt; for universal lock files across operating systems and programming languages&lt;/li&gt;&#10;&lt;li&gt;Provide a minimal Docker container that just installs the pixi lock file&lt;/li&gt;&#10;&lt;li&gt;Create self-documenting Jupyter notebooks that reproduce every result&lt;/li&gt;&#10;&lt;li&gt;Include clear figure-to-code mapping for paper reproducibility&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Balancing code cleanliness with research deadlines&lt;/li&gt;&#10;&lt;li&gt;Managing large datasets efficiently&lt;/li&gt;&#10;&lt;li&gt;Ensuring long-term reproducibility&lt;/li&gt;&#10;&lt;li&gt;Making complex physics simulations accessible&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Clean code takes time but saves more in the long run&lt;/li&gt;&#10;&lt;li&gt;Documentation is as crucial as the code itself&lt;/li&gt;&#10;&lt;li&gt;Lock files are essential for true reproducibility&lt;/li&gt;&#10;&lt;li&gt;Making code public improves its quality&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to explore quantum physics simulations? Check out the repositories above, each linked to their corresponding papers with full reproduction instructions.&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #OpenScience #Physics #QuantumComputing #Python&lt;/p&gt;&#10;</description></item><item><title>markdown-code-runner 📝</title><link>https://www.nijho.lt/post/advent-of-open-source/05-markdown-code-runner/</link><pubDate>Thu, 05 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/05-markdown-code-runner/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Documentation should be as close to the code as possible - that&amp;rsquo;s the principle of &amp;ldquo;information locality.&amp;rdquo; But how do you keep code examples and their outputs in sync? Enter markdown-code-runner, a tool that executes code blocks within Markdown files and updates their output in-place!&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While maintaining various open source and work projects, I found myself constantly updating README or Wiki files with new command outputs, updated tables, and fresh examples. This manual process was error-prone and tedious. Instead of using external templating files or more advanced documentation generators like Sphinx and Quarto (which are often overkill for single-page projects), I wanted something simpler: code and its output living together in the same Markdown file, always in sync.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Executes code blocks (Python and Bash) within Markdown files&lt;/li&gt;&#10;&lt;li&gt;Updates outputs in-place while preserving the original file structure&lt;/li&gt;&#10;&lt;li&gt;Supports hidden code blocks for generating content without showing the code&lt;/li&gt;&#10;&lt;li&gt;Works with any language by using file code blocks and bash execution&lt;/li&gt;&#10;&lt;li&gt;Zero external dependencies&lt;/li&gt;&#10;&lt;li&gt;GitHub Actions integration for automatic README updates&lt;/li&gt;&#10;&lt;li&gt;Preserves Markdown spec compatibility by hiding code in HTML comments (&lt;code&gt;&amp;lt;!-- CODE HERE --&amp;gt;&lt;/code&gt;)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;97 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;Used in many of my other projects for auto-generating:&#10;&lt;ul&gt;&#10;&lt;li&gt;Command-line help texts&lt;/li&gt;&#10;&lt;li&gt;API documentation&lt;/li&gt;&#10;&lt;li&gt;Example outputs&lt;/li&gt;&#10;&lt;li&gt;Data tables&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Helps maintain &amp;ldquo;information locality&amp;rdquo; - everything needed is in one file&lt;/li&gt;&#10;&lt;li&gt;Enables dynamic documentation that stays up-to-date automatically&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Parsing Markdown without breaking its structure&lt;/li&gt;&#10;&lt;li&gt;Supporting multiple programming languages&lt;/li&gt;&#10;&lt;li&gt;Making it work seamlessly with GitHub Actions&lt;/li&gt;&#10;&lt;li&gt;Handling complex outputs (tables, images, etc.)&lt;/li&gt;&#10;&lt;li&gt;Maintaining backward compatibility with existing Markdown files&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Documentation is best when it&amp;rsquo;s close to what it documents&lt;/li&gt;&#10;&lt;li&gt;Automation should be invisible - the tool shouldn&amp;rsquo;t change how you write Markdown&lt;/li&gt;&#10;&lt;li&gt;Sometimes the simplest solution (using HTML comments) is the best&lt;/li&gt;&#10;&lt;li&gt;Good documentation tools should make maintaining docs easier, not harder&lt;/li&gt;&#10;&lt;li&gt;Auto-generated content should be clearly marked as such&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to keep your documentation always in sync? Check out &lt;a href="https://github.com/basnijholt/markdown-code-runner" target="_blank" rel="noopener"&gt;markdown-code-runner on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #Documentation #DevTools #Programming&lt;/p&gt;&#10;</description></item><item><title>WenFire: When Could I retire?! 🔥</title><link>https://www.nijho.lt/post/advent-of-open-source/04-wenfire/</link><pubDate>Wed, 04 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/04-wenfire/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;While I absolutely love my job, I believe I&amp;rsquo;ll love it even more when my family&amp;rsquo;s livelihood doesn&amp;rsquo;t depend on it.&#10;That&amp;rsquo;s the dream of financial independence - doing what you love because you want to, not because you have to.&#10;This led me to create &lt;a href="https://wenfire.nijho.lt/" target="_blank" rel="noopener"&gt;WenFire&lt;/a&gt;, a financial independence calculator that helps answer the eternal questions: &amp;ldquo;When can I retire?&amp;rdquo; and &amp;ldquo;How many days of extra work will that new iPhone or car cost me?&amp;rdquo;&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;There are many retirement calculators out there, but they all made assumptions I didn&amp;rsquo;t quite agree with (and I&amp;rsquo;m sure others won&amp;rsquo;t agree with my assumptions either!).&#10;I wanted something that matched my specific views on salary growth and investment returns.&#10;Plus, I had been itching to learn more about building web applications, so this project let me explore FastAPI, htmx, and Vega-Lite while solving a personal need.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Built with FastAPI and Bootstrap&lt;/li&gt;&#10;&lt;li&gt;Interactive visualizations using Vega-Lite&lt;/li&gt;&#10;&lt;li&gt;Responsive and mobile-friendly design&lt;/li&gt;&#10;&lt;li&gt;Customizable assumptions for:&#10;&lt;ul&gt;&#10;&lt;li&gt;Investment growth&lt;/li&gt;&#10;&lt;li&gt;Inflation rates&lt;/li&gt;&#10;&lt;li&gt;Salary increases&lt;/li&gt;&#10;&lt;li&gt;Safe withdrawal rates&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;Allows parameter changes at certain dates&lt;/li&gt;&#10;&lt;li&gt;Calculate retirement impact of one-time expenses&lt;/li&gt;&#10;&lt;li&gt;All parameters are encoded in the URL for easy sharing&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While this project only has 3 GitHub stars, its real value is personal - it helps me track my own journey to financial independence.&#10;It&amp;rsquo;s also been a great learning experience in web development, an area where I had little prior experience.&lt;/p&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Learning web development from scratch&lt;/li&gt;&#10;&lt;li&gt;Creating intuitive visualizations for financial data&lt;/li&gt;&#10;&lt;li&gt;Balancing simplicity with customization options&lt;/li&gt;&#10;&lt;li&gt;Making reasonable default assumptions&lt;/li&gt;&#10;&lt;li&gt;Ensuring mobile responsiveness&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Financial modeling requires careful consideration of assumptions&lt;/li&gt;&#10;&lt;li&gt;Web development is a different beast from scientific computing&lt;/li&gt;&#10;&lt;li&gt;Sometimes the best way to learn new tech is to solve a personal problem&lt;/li&gt;&#10;&lt;li&gt;The best calculator is the one that matches your own assumptions&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want to calculate your path to financial independence? Try it at &lt;a href="https://wenfire.nijho.lt/" target="_blank" rel="noopener"&gt;wenfire.nijho.lt&lt;/a&gt; or check out the &lt;a href="https://github.com/basnijholt/wenfire" target="_blank" rel="noopener"&gt;source code&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #FinancialIndependence #WebDev #FIRE&lt;/p&gt;&#10;</description></item><item><title>rsync-time-machine.py 🕒</title><link>https://www.nijho.lt/post/advent-of-open-source/03-rsync-time-machine/</link><pubDate>Tue, 03 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/03-rsync-time-machine/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Sometimes the best open source projects start with &amp;ldquo;I can do better than this.&amp;rdquo; That&amp;rsquo;s exactly what happened when I looked at &lt;a href="https://github.com/laurent22/rsync-time-backup" target="_blank" rel="noopener"&gt;laurent22/rsync-time-backup&lt;/a&gt;, a popular 600-line Bash script for Time Machine-style backups. While it worked, it had known bugs and was tricky to maintain due to Bash&amp;rsquo;s limitations. It also lacked tests, type hints (bash duh&amp;hellip;), and proper error handling. So, I did what any reasonable developer would do: rewrote it in Python: &lt;a href="https://github.com/basnijholt/rsync-time-machine.py" target="_blank" rel="noopener"&gt;&lt;code&gt;rsync-time-machine.py&lt;/code&gt;&lt;/a&gt;! 🐍&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I needed reliable backups that worked across platforms and filesystems, unlike Apple&amp;rsquo;s Time Machine.&#10;While many excellent backup tools exist, I was drawn to the simplicity of this approach: it just creates folders named &lt;code&gt;YYYY-MM-DD-HH-MM-SS&lt;/code&gt; containing your files.&#10;No special tools needed for restoring or browsing - just your regular file explorer!&#10;I created a fully tested, typed, and documented Python port that maintains compatibility with the original while adding modern development practices.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Creates incremental backups using hard links (saving space while keeping full snapshots)&lt;/li&gt;&#10;&lt;li&gt;Works on Linux, macOS, and Windows (via WSL or Cygwin)&lt;/li&gt;&#10;&lt;li&gt;Supports local and remote (SSH) backups&lt;/li&gt;&#10;&lt;li&gt;Smart backup expiration strategy&lt;/li&gt;&#10;&lt;li&gt;Built-in safety checks and locking mechanism&lt;/li&gt;&#10;&lt;li&gt;Pretty terminal output&lt;/li&gt;&#10;&lt;li&gt;Absolutely zero dependencies beyond Python itself - it just works!&lt;/li&gt;&#10;&lt;li&gt;Usable as a standalone &lt;code&gt;.py&lt;/code&gt; script or pip installable package&lt;/li&gt;&#10;&lt;li&gt;100% test coverage&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The project reached the front page of &lt;a href="https://news.ycombinator.com/item?id=35933238" target="_blank" rel="noopener"&gt;Y Combinator&amp;rsquo;s Hacker News&lt;/a&gt;, causing its popularity to explode:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;381 GitHub stars&lt;/li&gt;&#10;&lt;li&gt;16 forks&lt;/li&gt;&#10;&lt;li&gt;6 contributors&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://realpython.com/podcasts/rpp/158/#t=1588" target="_blank" rel="noopener"&gt;Featured on Real Python podcast&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;Active discussion on Reddit&amp;rsquo;s &lt;a href="https://www.reddit.com/r/commandline/comments/13gtnrp/ported_a_popular_untested_600_line_bash_script_to/" target="_blank" rel="noopener"&gt;r/commandline&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Maintaining compatibility with the original Bash script while improving the codebase&lt;/li&gt;&#10;&lt;li&gt;Making it work on Windows through WSL/Cygwin (surprisingly tricky!)&lt;/li&gt;&#10;&lt;li&gt;Handling various edge cases in filesystem operations&lt;/li&gt;&#10;&lt;li&gt;Implementing a robust backup expiration strategy&lt;/li&gt;&#10;&lt;li&gt;Adding comprehensive test coverage for file operations&lt;/li&gt;&#10;&lt;li&gt;Keeping the codebase dependency-free while maintaining functionality&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Sometimes a rewrite is worth it, especially when adding modern development practices&lt;/li&gt;&#10;&lt;li&gt;Good documentation and test coverage are crucial for backup tools&lt;/li&gt;&#10;&lt;li&gt;The Python community appreciates well-structured ports of useful tools&lt;/li&gt;&#10;&lt;li&gt;Zero dependencies means zero headaches for users&lt;/li&gt;&#10;&lt;li&gt;Cross-platform compatibility (especially Windows) requires careful consideration&lt;/li&gt;&#10;&lt;li&gt;Simple, transparent solutions (like regular folders) can be better than complex ones&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want reliable, cross-platform Time Machine-style backups? Check out &lt;a href="https://github.com/basnijholt/rsync-time-machine.py" target="_blank" rel="noopener"&gt;the project on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #Backups #DevTools #Programming&lt;/p&gt;&#10;</description></item><item><title>Adaptive Lighting 💡</title><link>https://www.nijho.lt/post/advent-of-open-source/02-adaptive-lighting/</link><pubDate>Mon, 02 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/02-adaptive-lighting/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;Unlike &lt;a href="../01-calendar-of-life"&gt;yesterday&amp;rsquo;s Calendar of Life&lt;/a&gt; project, today we&amp;rsquo;re looking at one of my most popular contributions. It&amp;rsquo;s one of several Home Assistant and home automation related projects (&lt;a href="../08-home-assistant-stream-deck-yaml"&gt;Day 8&lt;/a&gt;, &lt;a href="../10-home-assistant-config"&gt;Day 10&lt;/a&gt;, &lt;a href="../24-home-assistant-ecosystem"&gt;Day 24&lt;/a&gt;) I&amp;rsquo;ll be sharing this month. While my coding journey started in scientific computing with physics simulations, discovering how to control my home environment through code sparked a new obsession. Home Assistant, currently the &lt;strong&gt;#1&lt;/strong&gt; open source project on GitHub, is the brain of my smart home and lets you control and automate everything from lights to thermostats, completely locally and privacy-focused.&lt;/p&gt;&#10;&lt;p&gt;Keep your lighting in sync with the sun, they said. It&amp;rsquo;ll be simple, they said&amp;hellip;&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Back in 2020, inspired by @claytonjn&amp;rsquo;s circadian_lighting Home Assistant component, I wanted to create a more comprehensive solution for automatically adjusting lights based on the time of day. What started as a contribution attempt to Home Assistant Core evolved into its own custom component when the PR wasn&amp;rsquo;t merged - handling all the edge cases made the code too complex for core. The standalone component gave us the freedom to implement features properly, becoming one of Home Assistant&amp;rsquo;s most installed custom integrations.&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;While the concept is simple, making it work reliably is&amp;hellip; interesting:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Handles unreliable Zigbee networks and flaky WiFi connections&lt;/li&gt;&#10;&lt;li&gt;Manages different light brands&amp;rsquo; quirks (looking at you, IKEA bulbs 👀)&lt;/li&gt;&#10;&lt;li&gt;Provides smooth transitions between color temperatures&lt;/li&gt;&#10;&lt;li&gt;Detects manual overrides without disrupting user control&lt;/li&gt;&#10;&lt;li&gt;Supports sleep modes for bedtime routines&lt;/li&gt;&#10;&lt;li&gt;Configurable via YAML or the UI (only integration to support this!)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The integration has 38 parameters to fine-tune your lighting. Try our &lt;a href="https://basnijholt.github.io/adaptive-lighting/" target="_blank" rel="noopener"&gt;WebAssembly simulator&lt;/a&gt; to visualize your settings!&lt;/p&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;2000+ GitHub stars&lt;/li&gt;&#10;&lt;li&gt;103 contributors&lt;/li&gt;&#10;&lt;li&gt;512 issues&lt;/li&gt;&#10;&lt;li&gt;Tens of thousands of active users&lt;/li&gt;&#10;&lt;li&gt;Available in 20+ languages&lt;/li&gt;&#10;&lt;li&gt;One of the most installed custom integrations&lt;/li&gt;&#10;&lt;li&gt;Featured in several YouTube videos and podcasts&lt;/li&gt;&#10;&lt;li&gt;Dedicated parameter visualization web app&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-challenges-we-solved"&gt;🎯 Challenges We Solved &lt;a class="anchor" href="#-challenges-we-solved" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Lights randomly turning on (bulbs falsely reporting state)&lt;/li&gt;&#10;&lt;li&gt;Network congestion (solved with Zigbee grouping)&lt;/li&gt;&#10;&lt;li&gt;Different brands interpreting colors differently&lt;/li&gt;&#10;&lt;li&gt;Handling manual overrides gracefully&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&amp;ldquo;It works on my setup&amp;rdquo; is just the beginning&lt;/li&gt;&#10;&lt;li&gt;Users surprise you (like using it for plants, or above the polar circle!)&lt;/li&gt;&#10;&lt;li&gt;Edge cases multiply quickly&lt;/li&gt;&#10;&lt;li&gt;Documentation is as important as code&lt;/li&gt;&#10;&lt;li&gt;Open source thrives on community effort&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Want your lights to follow the sun? Check out the &lt;a href="https://github.com/basnijholt/adaptive-lighting" target="_blank" rel="noopener"&gt;project on GitHub&lt;/a&gt; and try the &lt;a href="https://basnijholt.github.io/adaptive-lighting/" target="_blank" rel="noopener"&gt;simulator&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #HomeAssistant #SmartHome #IoT #Python&lt;/p&gt;&#10;</description></item><item><title>Calendar of Life 📅</title><link>https://www.nijho.lt/post/advent-of-open-source/01-calendar-of-life/</link><pubDate>Sun, 01 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/advent-of-open-source/01-calendar-of-life/</guid><description>&lt;p&gt;(See my &lt;a href="../"&gt;intro post&lt;/a&gt;.)&lt;/p&gt;&#10;&lt;p&gt;Visualizing the finite weeks of our lives, because apparently, I needed another reminder of my mortality!&lt;/p&gt;&#10;&lt;h2 id="-origin-story"&gt;📖 Origin Story &lt;a class="anchor" href="#-origin-story" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;You know how sometimes you&amp;rsquo;re just casually browsing the internet, not looking to have an existential crisis? Well, that&amp;rsquo;s exactly what happened when I stumbled upon Tim Urban&amp;rsquo;s Wait But Why post &amp;ldquo;Your Life in Weeks&amp;rdquo; and saw Kurzgesagt&amp;rsquo;s &amp;ldquo;Calendar of Your Life&amp;rdquo; poster. Instead of just feeling mildly anxious about the finite nature of existence like a normal person, I decided to cope by writing some Python code. Because that&amp;rsquo;s what we do, right?&lt;/p&gt;&#10;&lt;h2 id="-technical-highlights"&gt;🔧 Technical Highlights &lt;a class="anchor" href="#-technical-highlights" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The project uses matplotlib to create a grid where:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Each row represents one year (up to 80 years, optimistically speaking!)&lt;/li&gt;&#10;&lt;li&gt;Each column represents one week (52 weeks per year)&lt;/li&gt;&#10;&lt;li&gt;Different colors show different life stages (early childhood, school, university, etc.)&lt;/li&gt;&#10;&lt;li&gt;The current week blinks in the animated version, just in case you needed a more urgent reminder of time passing&lt;/li&gt;&#10;&lt;li&gt;Supports both dark and light modes, because existential dread comes in all themes&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;One interesting technical detail is handling the fact that 52 weeks isn&amp;rsquo;t exactly one year. The code uses a small correction factor (&lt;code&gt;days_per_week = 365.25 / 52&lt;/code&gt;) to ensure the visualization stays accurate over many years. Yes, I made sure your existential crisis would be mathematically precise.&lt;/p&gt;&#10;&lt;h2 id="-impact"&gt;📊 Impact &lt;a class="anchor" href="#-impact" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;With its whopping 5 GitHub stars, this project clearly hasn&amp;rsquo;t taken the world by storm. But hey, sometimes the best tools are the ones we make for ourselves, right?&lt;/p&gt;&#10;&lt;h2 id="-challenges-and-solutions"&gt;🎯 Challenges and Solutions &lt;a class="anchor" href="#-challenges-and-solutions" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The main challenges were (this was actually a very simple one):&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Accurately mapping life events to week numbers (turns out life doesn&amp;rsquo;t fit neatly into boxes)&lt;/li&gt;&#10;&lt;li&gt;Creating smooth animations for the current week indicator (because a blinking dot really drives home the whole &amp;ldquo;time is passing&amp;rdquo; thing)&lt;/li&gt;&#10;&lt;li&gt;Handling the 52 vs 52.18 weeks per year discrepancy (thanks, Earth&amp;rsquo;s orbit)&lt;/li&gt;&#10;&lt;li&gt;Making the visualization work well in both dark and light modes (for viewing your life calendar at any time of day)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="-lessons-learned"&gt;💡 Lessons Learned &lt;a class="anchor" href="#-lessons-learned" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This project taught me that sometimes the most impactful tools are the simplest ones. Sure, it&amp;rsquo;s just a bunch of dots on a grid, but each dot represents a week of life - past, present, or future. It&amp;rsquo;s a humbling reminder that while we can&amp;rsquo;t add more dots to our grid, we can try to make each one count.&lt;/p&gt;&#10;&lt;p&gt;Want to contemplate your own mortality in a more organized way? Check out the &lt;a href="https://github.com/basnijholt/calendar-of-life" target="_blank" rel="noopener"&gt;project on GitHub&lt;/a&gt;!&lt;/p&gt;&#10;&lt;p&gt;#OpenSource #Python #DataVisualization #Programming&lt;/p&gt;&#10;</description></item><item><title>opennb</title><link>https://www.nijho.lt/project/opennb/</link><pubDate>Wed, 27 Nov 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/opennb/</guid><description/></item><item><title>graphviz-anywidget</title><link>https://www.nijho.lt/project/graphviz-anywidget/</link><pubDate>Fri, 22 Nov 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/graphviz-anywidget/</guid><description/></item><item><title>clip-files</title><link>https://www.nijho.lt/project/clip-files/</link><pubDate>Thu, 13 Jun 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/clip-files/</guid><description/></item><item><title>Tiller Streamlit</title><link>https://www.nijho.lt/project/tiller-streamlit/</link><pubDate>Thu, 08 Feb 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/tiller-streamlit/</guid><description/></item><item><title>numthreads</title><link>https://www.nijho.lt/project/numthreads/</link><pubDate>Fri, 05 Jan 2024 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/numthreads/</guid><description/></item><item><title>UniDep</title><link>https://www.nijho.lt/project/unidep/</link><pubDate>Sat, 25 Nov 2023 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/unidep/</guid><description/></item><item><title>PipeFunc</title><link>https://www.nijho.lt/project/pipefunc/</link><pubDate>Thu, 15 Jun 2023 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/pipefunc/</guid><description/></item><item><title>rsync-time-machine.py</title><link>https://www.nijho.lt/project/rsync-time-machine.py/</link><pubDate>Thu, 11 May 2023 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/rsync-time-machine.py/</guid><description/></item><item><title>WenFire ⁉️🔥</title><link>https://www.nijho.lt/project/wenfire/</link><pubDate>Thu, 11 May 2023 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/wenfire/</guid><description/></item><item><title>Adaptive Lighting</title><link>https://www.nijho.lt/project/adaptive-lighting/</link><pubDate>Sat, 22 Apr 2023 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/adaptive-lighting/</guid><description/></item><item><title>CodeStructure</title><link>https://www.nijho.lt/project/codestructure/</link><pubDate>Sat, 22 Apr 2023 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/codestructure/</guid><description/></item><item><title>Home Assistant Streamdeck YAML</title><link>https://www.nijho.lt/project/home-assistant-streamdeck-yaml/</link><pubDate>Sat, 22 Apr 2023 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/home-assistant-streamdeck-yaml/</guid><description/></item><item><title>Markdown Code Runner</title><link>https://www.nijho.lt/project/markdown-code-runner/</link><pubDate>Sat, 22 Apr 2023 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/markdown-code-runner/</guid><description/></item><item><title>My favorite tools that keep my Python projects sane 🧰</title><link>https://www.nijho.lt/post/best-python-dev-tooling/</link><pubDate>Sat, 22 Apr 2023 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/best-python-dev-tooling/</guid><description>&lt;h2 id="-1-introduction"&gt;🌟 1. Introduction &lt;a class="anchor" href="#-1-introduction" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;As a passionate Python developer, I am always on the hunt for the best tools to streamline my development process and enhance the quality of my projects. Over time, I have discovered a set of tools that have become indispensable in my workflow, making my coding experience more efficient and enjoyable. Using the right developer tools for Python projects is crucial for maximizing productivity, ensuring code consistency, and maintaining high-quality code.&lt;/p&gt;&#10;&lt;p&gt;In this blog post, I will share these essential tools that I now use for every single new project. We will explore the benefits of each tool and discuss how they can help you supercharge your Python development. The tools covered in this post include &lt;a href="https://pre-commit.com/" target="_blank" rel="noopener"&gt;pre-commit&lt;/a&gt;, &lt;a href="https://black.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;Black&lt;/a&gt;, &lt;a href="https://beta.ruff.rs/docs/rules/" target="_blank" rel="noopener"&gt;Ruff&lt;/a&gt;, &lt;a href="https://mypy.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;Mypy&lt;/a&gt;, &lt;a href="https://versioningit.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Versioningit&lt;/a&gt;, &lt;a href="https://www.python.org/dev/peps/pep-0621/" target="_blank" rel="noopener"&gt;Pyproject.toml with PEP 621&lt;/a&gt;, &lt;a href="https://nox.thea.codes/en/stable/" target="_blank" rel="noopener"&gt;Nox&lt;/a&gt;, &lt;a href="https://docs.pytest.org/en/7.3.x/" target="_blank" rel="noopener"&gt;pytest&lt;/a&gt;, &lt;a href="https://docs.github.com/en/actions/learn-github-actions" target="_blank" rel="noopener"&gt;GitHub Actions CI matrix&lt;/a&gt;, and &lt;a href="https://readthedocs.org/" target="_blank" rel="noopener"&gt;Read the Docs&lt;/a&gt;, &lt;a href="https://www.sphinx-doc.org/" target="_blank" rel="noopener"&gt;Sphinx&lt;/a&gt;, &lt;a href="https://myst-nb.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;MyST-NB&lt;/a&gt;, and &lt;a href="https://jupytext.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Jupytext notebooks&lt;/a&gt; for documentation. Let&amp;rsquo;s dive in and learn how to enhance your Python projects with these powerful tools!&lt;/p&gt;&#10;&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#-1-introduction"&gt;🌟 1. Introduction&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-2-pre-commit"&gt;🛠️ 2. Pre-commit&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-3-black-auto-formatting-for-python"&gt;🎨 3. Black: auto-formatting for Python&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-4-ruff-the-fastest-python-linter-and-auto-formatter"&gt;🚀 4. Ruff: the fastest Python linter and auto-formatter&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-5-mypy-static-type-checking-for-python-find-bugs-before-they-happen"&gt;🔍 5. Mypy: static type checking for Python (find bugs before they happen)&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-6-nox-and-pytest-for-testing-a-powerful-combination"&gt;🧪 6. Nox and pytest for testing: a powerful combination&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-7-versioningit-version-management-with-git-tags"&gt;🏷️ 7. Versioningit: version management with Git tags&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-8-pyprojecttoml-and-pep-621-configuration-consolidation"&gt;📝 8. Pyproject.toml and PEP 621: Configuration Consolidation&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-9-github-actions-ci-matrix-testing-on-multiple-platforms-and-auto-publishing-to-pypi"&gt;🌐 9. GitHub Actions CI Matrix: Testing on Multiple Platforms and Auto Publishing to PyPI&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-10-read-the-docs-sphinx-myst-nb-and-jupytext-notebooks-for-documentation"&gt;📚 10. Read the Docs, Sphinx, MyST-NB, and Jupytext notebooks for documentation&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#-11-conclusion"&gt;🎉 11. Conclusion&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;This post is based the way I develop Python projects. For full examples see &lt;a href="https://github.com/basnijholt/adaptive-scheduler/" target="_blank" rel="noopener"&gt;Adaptive Scheduler&lt;/a&gt; (full project), &lt;a href="https://github.com/basnijholt/markdown-code-runner" target="_blank" rel="noopener"&gt;&lt;code&gt;markdown-code-runner&lt;/code&gt;&lt;/a&gt; (for a single module package), and &lt;a href="https://github.com/python-adaptive/adaptive/" target="_blank" rel="noopener"&gt;Adaptive&lt;/a&gt; (for cool documentation and nox).&#10;Over time I might change my mind about some of the tools, but I will try to keep this post up-to-date.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="-2-pre-commit"&gt;🛠️ 2. Pre-commit &lt;a class="anchor" href="#-2-pre-commit" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;One of the most common challenges developers face when working on a project, whether alone or in a team, is maintaining code quality and consistency. As projects grow, it becomes increasingly difficult to enforce coding standards and catch issues before they make their way into the codebase. This is where pre-commit comes in.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://pre-commit.com/" target="_blank" rel="noopener"&gt;Pre-commit&lt;/a&gt; is a powerful tool that helps you manage and maintain code quality by automating checks before commits. It achieves this by setting up a series of hooks that run before each commit, ensuring that only code that meets your predefined standards is committed to the repository. By catching issues early, pre-commit helps you keep your code clean and consistent, making it easier to review and maintain.&lt;/p&gt;&#10;&lt;p&gt;One of the reasons pre-commit has gained widespread adoption is its flexibility and extensibility. You can choose from a vast range of built-in hooks, such as code formatters, linters, and security checkers, or even create your own custom hooks. This allows you to tailor pre-commit to the specific needs of your project, ensuring that it enforces the exact coding standards you require.&lt;/p&gt;&#10;&lt;p&gt;Using pre-commit is beneficial for both individual developers and teams. For solo developers, it helps maintain discipline and enforces coding standards, making it easier to onboard collaborators in the future. For teams, it streamlines the review process by catching issues before they are submitted for review, reducing the time spent on addressing code inconsistencies and style violations.&lt;/p&gt;&#10;&lt;p&gt;Let&amp;rsquo;s take a look at an example. Suppose your project follows the PEP 8 coding standard, and you want to ensure that all code committed to the repository adheres to these guidelines. You can configure pre-commit to run a PEP 8 linter, such as Flake8, before each commit. This way, any code that does not meet the PEP 8 standard will be flagged, and the commit will be blocked until the issues are resolved.&lt;/p&gt;&#10;&lt;p&gt;In summary, pre-commit is an invaluable tool for maintaining code quality and consistency in your Python projects. By automating checks before commits, it ensures that your codebase remains clean and adheres to your chosen standards, making your development process more efficient and enjoyable.&lt;/p&gt;&#10;&lt;p&gt;Adding pre-commit to your Python project is simple and straightforward. To get started, you&amp;rsquo;ll first need to install the pre-commit package using pip:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pip install pre-commit&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, create a configuration file named &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; in your project&amp;rsquo;s root directory. This file will define the hooks you want to run before each commit. For example, to use Black for code formatting and Flake8 for linting, your configuration file might look like this:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://github.com/psf/black&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;stable&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;black&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://github.com/pycqa/flake8&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;master&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;flake8&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once you have set up the configuration file, run the following command to install the pre-commit hooks:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pre-commit install&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now, every time you attempt to make a commit, pre-commit will run the specified hooks. If any issues are found, the commit will be blocked, and you&amp;rsquo;ll be prompted to fix the problems before trying again. To run pre-commit manually on all files in your repository, use the following command:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pre-commit run --all-files&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By incorporating pre-commit into your development process, you&amp;rsquo;ll be able to catch and fix issues before they make their way into your codebase, ensuring a consistent and high-quality Python project.&lt;/p&gt;&#10;&lt;h2 id="-3-black-auto-formatting-for-python"&gt;🎨 3. Black: auto-formatting for Python &lt;a class="anchor" href="#-3-black-auto-formatting-for-python" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;As a developer who has always been particular about coding style, I&amp;rsquo;ve had my fair share of discussions on the proper way to indent multiline statements and other formatting nuances. These conversations, although sometimes necessary, can take up valuable time that could be better spent on actual development. That&amp;rsquo;s where Black, the uncompromising code formatter, comes in and saves the day.&lt;/p&gt;&#10;&lt;p&gt;Adopting &lt;a href="https://black.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;Black&lt;/a&gt; has made a significant impact on my development workflow. No more unnecessary discussions about formatting preferences; everyone on the team simply follows the rules set forth by Black. This consistency across the codebase not only makes the code more readable but also reduces the time spent on formatting debates.&lt;/p&gt;&#10;&lt;p&gt;One of the best aspects of Black is that you don&amp;rsquo;t need to worry about writing perfectly formatted code initially. You can write your code as quickly as you want, even if it&amp;rsquo;s poorly formatted, and then simply run Black to transform it into a beautifully formatted masterpiece. This can be a real time-saver, especially when working on large projects with many contributors.&lt;/p&gt;&#10;&lt;p&gt;To install Black, run the following command:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pip install black&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can then format your code by running Black in the terminal:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;black your_file.py&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Alternatively, you can use the VS Code command palette to run Black on the currently open file. However, the most efficient way to use Black is in combination with pre-commit, as mentioned in the previous section. This way, your code is automatically formatted every time you commit, ensuring a consistent codebase without any extra effort.&lt;/p&gt;&#10;&lt;p&gt;In summary, Black is an invaluable tool for enforcing consistent code formatting throughout your Python projects. By adopting it and integrating it with pre-commit, you can save time and focus on what truly matters: writing great code.&lt;/p&gt;&#10;&lt;h2 id="-4-ruff-the-fastest-python-linter-and-auto-formatter"&gt;🚀 4. Ruff: the fastest Python linter and auto-formatter &lt;a class="anchor" href="#-4-ruff-the-fastest-python-linter-and-auto-formatter" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;In the past, I&amp;rsquo;ve used several tools to maintain code quality, such as isort, pyupgrade, flake8, and pydocstyle. However, I&amp;rsquo;ve recently discovered a powerful new tool called &lt;a href="https://beta.ruff.rs/docs/rules/" target="_blank" rel="noopener"&gt;Ruff&lt;/a&gt;), an extremely fast Python linter written in Rust. Ruff is not only capable of replacing all these tools but also executes much faster, which significantly improves the development workflow.&lt;/p&gt;&#10;&lt;p&gt;Ruff offers an impressive range of features and performance enhancements. Some of its standout features include:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;10-100x faster than existing linters&lt;/li&gt;&#10;&lt;li&gt;Over 500 built-in rules&lt;/li&gt;&#10;&lt;li&gt;Autofix support for automatic error correction&lt;/li&gt;&#10;&lt;li&gt;Built-in caching to avoid re-analyzing unchanged files&lt;/li&gt;&#10;&lt;li&gt;First-party editor integrations for VS Code and more&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Moreover, Ruff has gained widespread adoption in the Python community and is used in major open-source projects like Apache Airflow, FastAPI, Hugging Face, Pandas, and SciPy. The tool is backed by Astral, which provides active development and support.&lt;/p&gt;&#10;&lt;p&gt;One of the best aspects of Ruff is the native VS Code extension, which continuously scans your code and can fix problems as you write. This seamless integration makes it an indispensable part of any Python development workflow.&lt;/p&gt;&#10;&lt;p&gt;To install Ruff, simply run the following command:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pip install ruff&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To use Ruff in your project, run the following command in the terminal:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ruff your_file.py&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For the best experience, I highly recommend using Ruff in conjunction with pre-commit, as mentioned earlier. This ensures that your code is always linted and formatted before committing, maintaining a consistent and high-quality codebase.&lt;/p&gt;&#10;&lt;p&gt;In conclusion, Ruff is an incredibly fast and powerful Python linter that streamlines the development process by consolidating multiple tools into a single, efficient package. By adopting Ruff and integrating it with pre-commit, you can enjoy a more productive and enjoyable Python development experience.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;Pro tip: Go extreme, and use &lt;code&gt;--select ALL&lt;/code&gt; to enable all (&amp;gt;500) rules. This will ensure that your code is always in top shape.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="-5-mypy-static-type-checking-for-python-find-bugs-before-they-happen"&gt;🔍 5. Mypy: static type checking for Python (find bugs before they happen) &lt;a class="anchor" href="#-5-mypy-static-type-checking-for-python-find-bugs-before-they-happen" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;As developers, we&amp;rsquo;re always looking for ways to improve the quality of our code and minimize the time spent on debugging. This is where &lt;a href="https://mypy.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;Mypy&lt;/a&gt; comes in. Mypy is a powerful static type checker for Python, designed to catch potential runtime errors before they occur. By using Mypy, you can ensure that your code adheres to type hints and that it runs smoothly without unexpected issues.&lt;/p&gt;&#10;&lt;p&gt;Mypy offers several advantages to Python developers, including:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Early detection of potential type errors, preventing many common bugs&lt;/li&gt;&#10;&lt;li&gt;Improved code readability and self-documentation through type annotations&lt;/li&gt;&#10;&lt;li&gt;Easier refactoring and code maintenance, as type annotations provide clear contracts between functions and modules&lt;/li&gt;&#10;&lt;li&gt;Better collaboration and communication within teams, since type annotations make the code&amp;rsquo;s intent and expectations more explicit&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Adding Mypy to an existing project may initially seem time-consuming and frustrating, as it might flag numerous issues that require attention. However, once you&amp;rsquo;ve addressed these concerns and properly set up Mypy, the benefits far outweigh the initial effort. In fact, the time you invest in fixing type-related issues will quickly be recovered as Mypy helps you prevent bugs and reduce debugging time.&lt;/p&gt;&#10;&lt;p&gt;For a long time, I too ignored Mypy and only typed my code without actually checking it with the tool. But after truly embracing Mypy and integrating it into my development process, I can confidently say that I will never go back. The value it brings to the table in terms of code quality, readability, and reliability is simply too great to ignore.&lt;/p&gt;&#10;&lt;p&gt;By proactively identifying and resolving type-related issues, Mypy not only saves you precious debugging time but also enhances the overall quality of your codebase. As you continue to rely on Mypy, you&amp;rsquo;ll find that it becomes an indispensable part of your Python development toolkit, ensuring that your projects remain robust and maintainable.&lt;/p&gt;&#10;&lt;p&gt;To start using Mypy in your project, first install it with the following command:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pip install mypy&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then, you can run Mypy on your Python files by executing the following command in your terminal:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mypy your_file.py&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For even better integration, you can add Mypy as a pre-commit hook, ensuring that your code is always checked for type consistency before being committed. This further strengthens the code quality and helps prevent potential runtime errors.&lt;/p&gt;&#10;&lt;p&gt;In summary, incorporating Mypy into your Python development workflow adds an extra layer of safety and clarity, making it easier to write high-quality, reliable code. By combining Mypy with other tools like Ruff and pre-commit, you can create a robust development environment that promotes productivity and code quality.&lt;/p&gt;&#10;&lt;h2 id="-6-nox-and-pytest-for-testing-a-powerful-combination"&gt;🧪 6. Nox and pytest for testing: a powerful combination &lt;a class="anchor" href="#-6-nox-and-pytest-for-testing-a-powerful-combination" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When developing software, it&amp;rsquo;s essential to ensure that your code is reliable and functions as expected. The key to achieving this goal is writing thorough tests, which not only help you identify issues early on but also prevent regressions in the future. While it&amp;rsquo;s often tempting to dive headfirst into coding, taking a step back and first writing tests for your solution can lead to more efficient and effective development. Even if this approach doesn&amp;rsquo;t come naturally, it&amp;rsquo;s crucial to write tests for every problem you encounter to keep your codebase stable and reliable.&lt;/p&gt;&#10;&lt;p&gt;In the realm of Python testing, &lt;a href="https://nox.thea.codes/en/stable/" target="_blank" rel="noopener"&gt;Nox&lt;/a&gt; and &lt;a href="https://docs.pytest.org" target="_blank" rel="noopener"&gt;pytest&lt;/a&gt; stand out as powerful tools that can greatly enhance your testing process. Nox is a versatile automation tool that simplifies running tests in multiple virtual environments, while pytest is a popular and feature-rich testing framework for Python. Together, these tools provide a formidable testing environment that simplifies test management and execution.&lt;/p&gt;&#10;&lt;p&gt;In the past, you may have used &lt;a href="https://tox.wiki/en/latest/" target="_blank" rel="noopener"&gt;Tox&lt;/a&gt;, a tool that manages test environments with a text-based configuration file (e.g., in &lt;code&gt;setup.cfg&lt;/code&gt; or &lt;code&gt;pyproject.toml&lt;/code&gt;). While Tox has served developers well for many years, including myself, Nox offers a more flexible alternative. Instead of relying on a text-based configuration, Nox takes advantage of a Python configuration file, providing virtually limitless configurability. This added flexibility makes it easy to create complex test matrices with different versions and dependencies, and even accommodate special conditions using simple if statements. After happily using Tox for years, I discovered Nox and found that it elevated my testing workflow to a whole new level.&lt;/p&gt;&#10;&lt;p&gt;To get started with Nox and pytest, create a basic &lt;code&gt;nox.py&lt;/code&gt; file and a sample test file for pytest. Here&amp;rsquo;s an example &lt;code&gt;nox.py&lt;/code&gt; configuration:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;nox&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@nox.session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;3.7&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;3.8&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;3.9&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-r&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;requirements.txt&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;-r&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;test-requirements.txt&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;pytest&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And an example test file for pytest:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pytest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_addition&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_subtraction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Beyond basic testing functionality, pytest boasts powerful features such as fixtures, mocking and patching, and parameterization. Pytest fixtures enable you to create reusable resources for your tests, while mocking and patching allow you to replace parts of your system with fake objects, isolating your tests from external dependencies. Parameterization, implemented using &lt;code&gt;pytest.mark.parametrize&lt;/code&gt;, enables running the same test with multiple sets of inputs and expected outputs.&lt;/p&gt;&#10;&lt;p&gt;In conclusion, leveraging Nox and pytest for testing offers numerous advantages for developers who strive to improve code reliability and automate their testing workflows. By harnessing Nox&amp;rsquo;s exceptional configurability and pytest&amp;rsquo;s extensive testing features, you&amp;rsquo;ll establish a robust testing environment that bolsters code quality and ensures optimal performance.&lt;/p&gt;&#10;&lt;h2 id="-7-versioningit-version-management-with-git-tags"&gt;🏷️ 7. Versioningit: version management with Git tags &lt;a class="anchor" href="#-7-versioningit-version-management-with-git-tags" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;(Optional, only related when making a Python package)&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;For developers seeking an automated and customizable package versioning solution, &lt;a href="https://versioningit.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Versioningit&lt;/a&gt; offers a powerful yet flexible approach. It is a setuptools plugin that automatically determines your package&amp;rsquo;s version based on your version control repository&amp;rsquo;s tags. The real advantage of Versioningit lies in its ability to customize the version format and even override separate functions used for version extraction and calculation.&lt;/p&gt;&#10;&lt;p&gt;Versioningit makes your release workflow extremely simple, especially if you have set up an automated build and twine PyPI upload pipeline. To create a new release, you only need to tag a version, and then click on the &amp;ldquo;Release&amp;rdquo; button in GitHub.&lt;/p&gt;&#10;&lt;p&gt;One of the major advantages of Versioningit is its minimal boilerplate code compared to other tools like &lt;a href="https://github.com/python-versioneer/python-versioneer" target="_blank" rel="noopener"&gt;Versioneer&lt;/a&gt;. Additionally, the Python &lt;code&gt;__version__&lt;/code&gt; string works seamlessly with editable installs (&lt;code&gt;pip install -e .&lt;/code&gt;), unlike &lt;a href="https://github.com/pypa/setuptools_scm" target="_blank" rel="noopener"&gt;setuptools_scm&lt;/a&gt;, which is a common issue faced by developers.&lt;/p&gt;&#10;&lt;p&gt;Over the years, I have explored many options for package versioning, including &lt;a href="https://github.com/python-versioneer/python-versioneer" target="_blank" rel="noopener"&gt;Versioneer&lt;/a&gt;, &lt;a href="https://github.com/jbweston/miniver" target="_blank" rel="noopener"&gt;Miniver&lt;/a&gt;, &lt;a href="https://github.com/pypa/setuptools_scm" target="_blank" rel="noopener"&gt;setuptools_scm&lt;/a&gt;, &lt;a href="https://github.com/peritus/bumpversion" target="_blank" rel="noopener"&gt;bumpversion&lt;/a&gt; (or &lt;a href="https://github.com/c4urself/bump2version" target="_blank" rel="noopener"&gt;bump2version&lt;/a&gt;), and manually setting everything. Versioningit stands out due to its simplicity, customization options, and compatibility with various workflows.&lt;/p&gt;&#10;&lt;p&gt;Some key features of Versioningit include:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Support for Git, modern Git archives, and Mercurial&lt;/li&gt;&#10;&lt;li&gt;Customizable version formatting using template strings&lt;/li&gt;&#10;&lt;li&gt;Optional writing of the final version and other details to a file for loading at runtime&lt;/li&gt;&#10;&lt;li&gt;Custom setuptools commands for inserting the final version and other details into a source file at build time&lt;/li&gt;&#10;&lt;li&gt;Customizable functions for VCS querying, tag-to-version calculation, version bumping, version formatting, and writing the version to a file&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;To use Versioningit in your project, simply add it to your project&amp;rsquo;s &lt;code&gt;pyproject.toml&lt;/code&gt; file in the &lt;code&gt;build-system.requires&lt;/code&gt; key. Then, create a &lt;code&gt;[tool.versioningit]&lt;/code&gt; table in your &lt;code&gt;pyproject.toml&lt;/code&gt; file. You can get up and running with just the minimal configuration - an empty table.&lt;/p&gt;&#10;&lt;p&gt;Once you have a &lt;code&gt;[tool.versioningit]&lt;/code&gt; table in your &lt;code&gt;pyproject.toml&lt;/code&gt; and your repository has at least one tag, building your project with &lt;code&gt;setuptools&lt;/code&gt; will automatically set your project&amp;rsquo;s version based on the latest tag in your Git repository. With Versioningit, you can customize the version format using placeholder strings, making it adaptable to your project&amp;rsquo;s specific needs.&lt;/p&gt;&#10;&lt;p&gt;In summary, Versioningit streamlines the package versioning process and simplifies the release workflow. By automating package versioning based on version control repository tags and providing extensive customization options, it helps make the management of package versions more efficient and less error-prone.&lt;/p&gt;&#10;&lt;h2 id="-8-pyprojecttoml-and-pep-621-configuration-consolidation"&gt;📝 8. Pyproject.toml and PEP 621: Configuration Consolidation &lt;a class="anchor" href="#-8-pyprojecttoml-and-pep-621-configuration-consolidation" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The &lt;code&gt;pyproject.toml&lt;/code&gt; file plays a crucial role in consolidating configurations and setup for various tools in a Python project, such as Black, Ruff, Mypy, Versioningit, Nox, and pytest. This adherence to &lt;a href="https://www.python.org/dev/peps/pep-0621/" target="_blank" rel="noopener"&gt;PEP 621&lt;/a&gt; standards streamlines the development process by providing a single source of truth for multiple tools, eliminating the need for additional configuration files like &lt;code&gt;setup.cfg&lt;/code&gt; or &lt;code&gt;setup.py&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;By specifying packaging details directly in the &lt;code&gt;pyproject.toml&lt;/code&gt; file, you can efficiently manage your project&amp;rsquo;s dependencies and metadata. For example, the Adaptive Scheduler package demonstrates this approach with a comprehensive &lt;code&gt;pyproject.toml&lt;/code&gt; file containing all the necessary configurations.&lt;/p&gt;&#10;&lt;details&gt;&#10;&lt;summary&gt;(click to unfold):&lt;/summary&gt;&#10;&lt;!-- CODE:START --&gt;&#10;&lt;!-- import urllib.request --&gt;&#10;&lt;!-- url = 'https://raw.githubusercontent.com/basnijholt/adaptive-scheduler/main/pyproject.toml' --&gt;&#10;&lt;!-- print("```toml") --&gt;&#10;&lt;!-- with urllib.request.urlopen(url) as response: --&gt;&#10;&lt;!-- content = response.read() --&gt;&#10;&lt;!-- print(content.decode('utf-8')) --&gt;&#10;&lt;!-- print("```") --&gt;&#10;&lt;!-- CODE:END --&gt;&#10;&lt;!-- OUTPUT:START --&gt;&#10;&lt;!-- ⚠️ This content is auto-generated by `markdown-code-runner`. --&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;build-system&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;build-backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;setuptools.build_meta&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;requires&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;setuptools&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;wheel&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;versioningit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;adaptive_scheduler&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Run many `adaptive.Learner`s on many cores (&amp;gt;10k) using `mpi4py.futures`, `ipyparallel`, `dask-mpi`, or `process-pool`.&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;gt;=3.8&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;version&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;maintainers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Bas Nijholt&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;bas@nijho.lt&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;license&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;BSD-3&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;classifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Development Status :: 2 - Pre-Alpha&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Intended Audience :: Developers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Intended Audience :: Science/Research&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;License :: OSI Approved :: BSD License&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Operating System :: OS Independent&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Programming Language :: Python&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Programming Language :: Python :: 3&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Programming Language :: Python :: 3.8&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Programming Language :: Python :: 3.9&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Programming Language :: Python :: 3.10&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Programming Language :: Python :: 3.11&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Topic :: Scientific/Engineering&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Topic :: System :: Distributed Computing&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;adaptive &amp;gt;= 0.14.1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;cloudpickle&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;dill&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;ipyparallel&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;ipywidgets&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;itables&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;jinja2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;loky&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;numpy&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;pandas&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;psutil&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;pyarrow&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;pyzmq&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;rich&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;structlog&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;tinydb&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;toolz&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;tqdm&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;versioningit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;optional-dependencies&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;dask-mpi&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;mpi4py&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;pytest&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pytest-asyncio&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;coverage&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pytest-cov&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;docs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;myst-nb&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;sphinx_fontawesome&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;sphinx&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;furo&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;myst-parser&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;emoji&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;homepage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://adaptive-scheduler.readthedocs.io/&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;documentation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://adaptive-scheduler.readthedocs.io/&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://github.com/basnijholt/adaptive-scheduler&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readme&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;content-type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;text/x-rst&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;README.rst&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scripts&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;adaptive-scheduler-launcher&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;adaptive_scheduler._server_support.launcher:main&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;versioningit&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;versioningit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onbuild&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;build-file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;adaptive_scheduler/_version.py&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;source-file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;adaptive_scheduler/_version.py&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setuptools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;adaptive_scheduler.*&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;adaptive_scheduler&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ini_options&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;addopts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; -vvv&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; --cov=adaptive_scheduler&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; --cov-report term&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; --cov-report html&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; --cov-report xml&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; --cov-fail-under=35&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; --asyncio-mode=auto&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;exclude_lines&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;pragma: no cover&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;raise NotImplementedError&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;if TYPE_CHECKING:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;if __name__ == .__main__.:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;black&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;line_length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;88&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ruff&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;line-length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;target-version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;py38&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;select&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;ALL&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ignore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;T20&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# flake8-print&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;ANN101&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# Missing type annotation for {name} in method&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;S101&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# Use of assert detected&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;PD901&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# df is a bad variable name. Be kinder to your future self.&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;ANN401&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# Dynamically typed expressions (typing.Any) are disallowed in {name}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;D402&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# First line should not be the function&amp;#39;s signature&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;PLW0603&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# Using the global statement to update `X` is discouraged&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;D401&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# First line of docstring should be in imperative mood&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;SLF001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;# Private member accessed&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ruff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;per-file-ignores&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;tests/*&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;SLF001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;tests/test_examples.py&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;E501&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;.github/*&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;INP001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ruff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mccabe&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;max-complexity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mypy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;python_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;3.8&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;!-- OUTPUT:END --&gt;&#10;&lt;/details&gt;&#10;&lt;p&gt;(from &lt;a href="https://github.com/basnijholt/adaptive-scheduler/blob/main/pyproject.toml" target="_blank" rel="noopener"&gt;here&lt;/a&gt;)&lt;/p&gt;&#10;&lt;p&gt;As seen in the example, the &lt;code&gt;pyproject.toml&lt;/code&gt; file includes details about the build system, project metadata, dependencies, optional dependencies, URLs, and configurations for various tools such as Versioningit, setuptools, pytest, coverage, Black, Ruff, and Mypy. By consolidating these configurations, you can maintain a more organized and maintainable project, making it easier to understand and update settings for different tools.&lt;/p&gt;&#10;&lt;p&gt;In summary, the &lt;code&gt;pyproject.toml&lt;/code&gt; file and adherence to PEP 621 provide a unified approach to configuring and setting up your Python project, making it easier to manage dependencies, metadata, and tool-specific settings. This streamlined process ultimately leads to a more efficient and maintainable development workflow.&lt;/p&gt;&#10;&lt;h2 id="-9-github-actions-ci-matrix-testing-on-multiple-platforms-and-auto-publishing-to-pypi"&gt;🌐 9. GitHub Actions CI Matrix: Testing on Multiple Platforms and Auto Publishing to PyPI &lt;a class="anchor" href="#-9-github-actions-ci-matrix-testing-on-multiple-platforms-and-auto-publishing-to-pypi" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://docs.github.com/en/actions/learn-github-actions" target="_blank" rel="noopener"&gt;GitHub Actions&lt;/a&gt; is an excellent tool for automating workflows and improving code quality through continuous integration (CI). By using a CI matrix, you can test your code on multiple platforms and Python versions, ensuring that your package remains compatible and stable across different environments.&lt;/p&gt;&#10;&lt;p&gt;To set up a &lt;code&gt;.github/workflows/nox.yaml&lt;/code&gt; file for GitHub Actions, you can use the following simplified example:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nox&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;main]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ matrix.platform }}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;fail-fast&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest, macos-latest, windows-latest]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;3.8&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;3.9&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;3.10&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;3.11&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v2&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Set up Python ${{ matrix.python-version }}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/setup-python@v2&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ matrix.python-version }}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Install dependencies&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;pip install nox&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Test with nox&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nox -e py${{ matrix.python-version }}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This configuration file specifies the platforms and Python versions for testing, as well as the steps to set up the environment, install dependencies, and run tests using nox.&#10;With this simple configuration, you set up tests on MacOS, Windows, Linux, and multiple Python versions, ensuring that your package remains compatible across different environments.&lt;/p&gt;&#10;&lt;p&gt;Another essential part of the CI/CD process is deploying your Python package. You can use the &lt;code&gt;.github/workflows/publish-package.yaml&lt;/code&gt; file to automate the package deployment to PyPI whenever a new release is published:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Upload Python Package&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;release&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;published]&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v2&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Set up Python&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/setup-python@v2&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;3.x&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Install dependencies&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; python -m pip install --upgrade pip&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; pip install setuptools wheel twine build&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Build and publish&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;TWINE_USERNAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;__token__&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;TWINE_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.PYPI_TOKEN }}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; python -m build&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; twine upload dist/*&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This workflow sets up Python, installs necessary dependencies, builds the package, and publishes it to PyPI using Twine. The process integrates seamlessly with versioningit, ensuring that the published package has the correct version number based on the git history.&lt;/p&gt;&#10;&lt;p&gt;In summary, leveraging GitHub Actions for continuous integration and automated workflows enhances code quality and reliability, while also simplifying the development and deployment processes. By testing across various platforms and Python versions, and automating package deployment, you can be confident that your package is well-maintained and accessible to users.&lt;/p&gt;&#10;&lt;h2 id="-10-read-the-docs-sphinx-myst-nb-and-jupytext-notebooks-for-documentation"&gt;📚 10. Read the Docs, Sphinx, MyST-NB, and Jupytext notebooks for documentation &lt;a class="anchor" href="#-10-read-the-docs-sphinx-myst-nb-and-jupytext-notebooks-for-documentation" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Documentation is crucial for any software project, as it helps users understand how to use the package and provides guidance for contributors. &lt;a href="https://readthedocs.org/" target="_blank" rel="noopener"&gt;Read the Docs&lt;/a&gt;, &lt;a href="https://www.sphinx-doc.org/" target="_blank" rel="noopener"&gt;Sphinx&lt;/a&gt;, &lt;a href="https://myst-nb.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;MyST-NB&lt;/a&gt;, and &lt;a href="https://jupytext.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Jupytext notebooks&lt;/a&gt; are powerful tools that, when combined, facilitate the creation and maintenance of high-quality documentation.&lt;/p&gt;&#10;&lt;p&gt;tl;dr: check out the &lt;a href="https://adaptive.readthedocs.io/" target="_blank" rel="noopener"&gt;Adaptive documentation&lt;/a&gt; for an example of how to use these tools to create a comprehensive documentation website.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://readthedocs.org/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Read the Docs&lt;/strong&gt;&lt;/a&gt; is a documentation hosting platform that automatically builds, version controls, and hosts your documentation for free. It integrates well with Sphinx and GitHub, allowing your documentation to be updated whenever you push changes to your repository.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://www.sphinx-doc.org/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Sphinx&lt;/strong&gt;&lt;/a&gt; is a documentation generator that can transform reStructuredText files, MyST files, or Jupyter notebooks into various output formats, such as HTML, LaTeX, or PDF. It is highly extensible, supporting custom themes and plugins, and can automatically generate API documentation from your Python code.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://myst-nb.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;&lt;strong&gt;MyST-NB&lt;/strong&gt;&lt;/a&gt; is an extension to Sphinx that enables it to parse Jupyter notebooks and execute the code cells during the build process. It uses the MyST (short for Markedly Structured Text) markdown parser, which adds support for Sphinx roles and directives, making it a powerful alternative to reStructuredText.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://jupytext.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;&lt;strong&gt;Jupytext notebooks&lt;/strong&gt;&lt;/a&gt; is a Jupyter extension that enables you to save Jupyter notebooks in Markdown format (or other text-based formats) instead of the default JSON format. By using Jupytext notebooks, you can store your code, explanations, and visualizations as plain text, which is easier to version control and can be seamlessly integrated with MyST-NB and Sphinx.&lt;/p&gt;&#10;&lt;p&gt;With &lt;a href="https://myst-nb.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;MyST-NB&lt;/a&gt; and &lt;a href="https://jupytext.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Jupytext&lt;/a&gt;, you can write your documentation using Jupyter notebooks in Markdown format. This approach combines the benefits of Jupyter notebooks, such as interactive code execution and rich output, with the simplicity of Markdown and the powerful features of Sphinx. The result is a highly readable, user-friendly, and maintainable documentation that caters to both users and developers.&lt;/p&gt;&#10;&lt;p&gt;To get started with MyST-NB, you&amp;rsquo;ll need to install it using pip:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pip install myst-nb&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, configure your Sphinx project to use MyST-NB by adding the following lines to your &lt;code&gt;conf.py&lt;/code&gt; file:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;extensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;myst_nb&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now, Sphinx will be able to parse and execute your Jupyter notebooks during the documentation build process, integrating them seamlessly with your existing documentation.&lt;/p&gt;&#10;&lt;p&gt;In summary, using Read the Docs, Sphinx, MyST-NB, and Jupytext notebooks for your documentation enables you to create high-quality, interactive, and easily maintainable documentation that benefits your project&amp;rsquo;s users and contributors. This combination of tools ensures that your documentation remains up-to-date, easily navigable, and engaging, ultimately enhancing the overall experience for your project&amp;rsquo;s audience.&lt;/p&gt;&#10;&lt;p&gt;For a full example, see how I setup &lt;a href="https://adaptive.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Adaptive&amp;rsquo;s documentation&lt;/a&gt; using Sphinx, MyST-NB, and Jupytext notebooks. &lt;a href="https://github.com/python-adaptive/adaptive/blob/1b0f15e44235643731c56d335f6711c5584d4828/readthedocs.yml" target="_blank" rel="noopener"&gt;&lt;code&gt;readthedocs.yaml&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/python-adaptive/adaptive/blob/1b0f15e44235643731c56d335f6711c5584d4828/docs/source/conf.py" target="_blank" rel="noopener"&gt;&lt;code&gt;docs/source/conf.py&lt;/code&gt;&lt;/a&gt;, and here is an example notebook that is used in the &lt;a href="https://adaptive.readthedocs.io/en/latest/tutorial/tutorial.Learner1D.html" target="_blank" rel="noopener"&gt;documentation&lt;/a&gt;: &lt;a href="https://github.com/python-adaptive/adaptive/blob/main/docs/source/tutorial/tutorial.Learner1D.md" target="_blank" rel="noopener"&gt;&lt;code&gt;docs/source/tutorial/tutorial.Learner1D.md&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;&#10;&lt;h2 id="-11-conclusion"&gt;🎉 11. Conclusion &lt;a class="anchor" href="#-11-conclusion" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;In conclusion, incorporating these powerful tools into your Python development workflow can significantly enhance your productivity and code quality. By adopting &lt;a href="https://pre-commit.com/" target="_blank" rel="noopener"&gt;pre-commit&lt;/a&gt;, &lt;a href="https://black.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;Black&lt;/a&gt;, &lt;a href="https://beta.ruff.rs/docs/rules/" target="_blank" rel="noopener"&gt;Ruff&lt;/a&gt;, &lt;a href="https://mypy.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;Mypy&lt;/a&gt;, &lt;a href="https://versioningit.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Versioningit&lt;/a&gt;, &lt;a href="https://www.python.org/dev/peps/pep-0621/" target="_blank" rel="noopener"&gt;Pyproject.toml with PEP 621&lt;/a&gt;, &lt;a href="https://nox.thea.codes/en/stable/" target="_blank" rel="noopener"&gt;Nox&lt;/a&gt;, &lt;a href="https://docs.pytest.org/" target="_blank" rel="noopener"&gt;pytest&lt;/a&gt;, &lt;a href="https://docs.github.com/en/actions/learn-github-actions" target="_blank" rel="noopener"&gt;GitHub Actions CI matrix&lt;/a&gt;, and &lt;a href="https://readthedocs.org/" target="_blank" rel="noopener"&gt;Read the Docs&lt;/a&gt;, &lt;a href="https://www.sphinx-doc.org/" target="_blank" rel="noopener"&gt;Sphinx&lt;/a&gt;, &lt;a href="https://myst-nb.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;MyST-NB&lt;/a&gt;, and &lt;a href="https://jupytext.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;Jupytext notebooks&lt;/a&gt; for documentation, you create a streamlined, efficient, and reliable development environment.&lt;/p&gt;&#10;&lt;p&gt;These tools help maintain code consistency, catch issues early, automate package versioning, simplify testing, consolidate configurations, and create high-quality documentation. By integrating them into your projects, you can focus on writing great code while ensuring that your work remains robust, maintainable, and accessible to users and contributors alike.&lt;/p&gt;&#10;&lt;p&gt;Don&amp;rsquo;t hesitate to explore these tools and incorporate them into your Python development toolkit. They can transform your development experience, making it more enjoyable and efficient, ultimately leading to higher-quality projects and happier developers. Happy coding!&lt;/p&gt;&#10;</description></item><item><title>Calendar of Life</title><link>https://www.nijho.lt/project/calendar-of-life/</link><pubDate>Thu, 03 Jun 2021 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/calendar-of-life/</guid><description/></item><item><title>home-assistant-macbook-touch-bar</title><link>https://www.nijho.lt/project/home-assistant-macbook-touch-bar/</link><pubDate>Tue, 09 Jun 2020 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/home-assistant-macbook-touch-bar/</guid><description/></item><item><title>Thesis cover</title><link>https://www.nijho.lt/project/thesis-cover/</link><pubDate>Mon, 11 May 2020 15:00:00 +0000</pubDate><guid isPermaLink="false">/project/thesis-cover/</guid><description/></item><item><title>Crafting unique Ph.D. thesis covers with Python 🎨</title><link>https://www.nijho.lt/post/thesis-cover/</link><pubDate>Sat, 04 Apr 2020 13:40:00 +0000</pubDate><guid isPermaLink="false">/post/thesis-cover/</guid><description>&lt;p&gt;&lt;em&gt;Originally posted on &lt;a href="https://quantumtinkerer.tudelft.nl/blog/thesis-cover/" target="_blank" rel="noopener"&gt;quantumtinkerer.tudelft.nl/blog/thesis-cover&lt;/a&gt; on 2020-04-04 13:40.&lt;/em&gt;&lt;/p&gt;&#10;&lt;p&gt;I will hand out around a hundred copies of my thesis to my defense committee, colleagues, and my family and friends.&#10;Let&amp;rsquo;s be honest, most people will probably not get further than attempting to read the summary and appreciating the cover.&#10;Four years of work has gone into generating the content of the thesis, so I figured, at least some thought has to go into the design of its cover.&#10;Unfortunately, I am by no means an expert on a graphical design or even competent enough to attempt to use any kind of graphic design software.&#10;But luckily for me, I do consider myself an expert in Python &lt;i class="fab fa-python"&gt;&lt;/i&gt;, and why not make the thesis design a fun process?&lt;/p&gt;&#10;&lt;h2 id="initial-design-adaptive-sampling-and-data-visualization"&gt;Initial Design: Adaptive Sampling and Data Visualization &lt;a class="anchor" href="#initial-design-adaptive-sampling-and-data-visualization" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My vague plan was to somehow incorporate a plot resulting from a simulation run with &lt;a href="https://adaptive.readthedocs.io/" target="_blank" rel="noopener"&gt;Adaptive&lt;/a&gt; [&lt;a href="https://github.com/python-adaptive/adaptive" target="_blank" rel="noopener"&gt;source&lt;/a&gt; &lt;i class="fab fa-github"&gt;&lt;/i&gt;], the software I wrote during my Ph.D. to adaptively sample functions in parallel and my proudest work.&#10;I even had a potential simulation result in mind that I would use for it, specifically, one that has been on the background of my good friend’s phone for some years and one that has earned me quite some internet points on &lt;a href="https://www.reddit.com/r/dataisbeautiful/comments/eaap3p/python_quantum_research_art_saving_1000s_hours_of/" target="_blank" rel="noopener"&gt;Reddit&lt;/a&gt;.&#10;This plot (below) is one of the results of Chapter 4 of &lt;a href="http://files.nijho.lt/thesis.pdf" target="_blank" rel="noopener"&gt;my thesis&lt;/a&gt; [&lt;a href="https://github.com/basnijholt/thesis" target="_blank" rel="noopener"&gt;source&lt;/a&gt; &lt;i class="fab fa-github"&gt;&lt;/i&gt;] and &lt;a href="https://arxiv.org/abs/1807.01940%ef%bf%bc" target="_blank" rel="noopener"&gt;this paper&lt;/a&gt;.&lt;/p&gt;&#10;&lt;video style="width: 100%;max-width: 500px;" controls&gt;&#10; &lt;source src="https://user-images.githubusercontent.com/6897215/233811442-ce9bc475-af15-4b89-9578-67925419b769.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10;&lt;/video&gt;&#10;&lt;p&gt;I went back to the code that generates the plot and started hacking away in Matplotlib.&#10;Quickly, I realized that not only the plot above was going to look cool, but that many other data sets that I had also resulted in beautiful patterns.&#10;I hit the paradox of choice: I had a total of &lt;em&gt;3671&lt;/em&gt; unique data sets (all for Majorana devices with either different parameter sweeps or different physical effects that were included in the model) and no idea how I was going to make a selection.&lt;/p&gt;&#10;&lt;p&gt;This is an early attempt (and rather ugly in hindsight)&lt;/p&gt;&#10;&lt;figure&gt;&lt;img src="https://user-images.githubusercontent.com/6897215/233811538-dc3b5b86-6f6f-4bd6-840c-e466918ce151.jpeg" alt="early-attempt" loading="lazy" data-zoomable&gt;&lt;/figure&gt;&lt;p&gt;I decided to postpone the choice of which data to use and just focus on how to plot the data first.&#10;I experimented with different colormaps, fading colormaps that gradually reveal the underlying triangulation better, and more.&#10;On a weekend night, after telling &lt;a href="https://github.com/sbalk" target="_blank" rel="noopener"&gt;a friend&lt;/a&gt; I was with about my plans, we (as usually happens) started to program some of the ideas we came up with.&#10;We settled on a design where the triangulation would slowly fade from the top to bottom and where each triangle simultaneously transitioned from a solid color (the average of the values at its vertices) to densely interpolated values.&#10;See &lt;a href="https://github.com/basnijholt/thesis-cover/blob/master/generate-covers.ipynb" target="_blank" rel="noopener"&gt;this Jupyter notebook&lt;/a&gt; which generated the final version of the cover.&#10;After we also chose a nice colormap (by simply creating the cover for each possible colormap,) the following was suggested: “You know what would be even cooler?! To make every cover unique.”&#10;Ten minutes later, I had started 3671 jobs on our computational cluster.&#10;When the files slowly started to appear on my computer (I have my local files synced with the cluster), we could not have been more excited.&#10;There were simply so many cool looking alternatives.&#10;Too many to manually select now, to be honest&amp;mdash;a problem for later I thought.&lt;/p&gt;&#10;&lt;h2 id="overcoming-the-paradox-of-choice-clustering-and-neural-networks"&gt;Overcoming the Paradox of Choice: Clustering and Neural Networks &lt;a class="anchor" href="#overcoming-the-paradox-of-choice-clustering-and-neural-networks" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Four months later, with the rest of my thesis completely done and about a day left on the deadline to submit my thesis, I found myself facing the selection problem I had successfully ignored for so long.&#10;There must be a smarter way to select the covers, I thought.&#10;Many of them look roughly similar (and some of them are even quite boring or not ecstatically pleasing), so it does not make sense to scroll through all of them.&#10;I could cluster them by similarity and then I would just have to choose the nicely looking clusters while being able to instantly discard many.&lt;/p&gt;&#10;&lt;p&gt;Finally, a good application to use neural networks!&lt;/p&gt;&#10;&lt;p&gt;I still had the 3671 covers in pdf format in some folder, so using the wonderful &lt;a href="https://www.gnu.org/software/parallel/" target="_blank" rel="noopener"&gt;GNU Parallel&lt;/a&gt;, I converted and rasterized all of them to jpeg with&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find . -iname &lt;span class="se"&gt;\*&lt;/span&gt;pdf -print0 &lt;span class="p"&gt;|&lt;/span&gt; parallel -0 sips -s format jpeg &lt;span class="o"&gt;{}&lt;/span&gt; --out &lt;span class="o"&gt;{&lt;/span&gt;.&lt;span class="o"&gt;}&lt;/span&gt;.jpg&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then I used a pre-trained convolutional neural network model (&lt;a href="https://keras.io/applications/#vgg16" target="_blank" rel="noopener"&gt;VGG16&lt;/a&gt;) to extract features in a lower-dimensional representation.&#10;I fed the feature arrays to a &lt;a href="https://en.wikipedia.org/wiki/K-means_clustering" target="_blank" rel="noopener"&gt;K-Means clustering algorithm&lt;/a&gt; that sorted them in 80 clusters.&#10;To procrastinate a little more, I created an interactive widget using &lt;a href="https://ipywidgets.readthedocs.io/" target="_blank" rel="noopener"&gt;&lt;code&gt;ipywidgets&lt;/code&gt;&lt;/a&gt; (see video below) to facilitate the selection process.&lt;/p&gt;&#10;&lt;video style="width: 100%;max-width: 800px;" controls&gt;&#10; &lt;source src="https://user-images.githubusercontent.com/6897215/233811575-98ea0be2-33f4-47f4-b3ee-f8e7bdf3c475.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10;&lt;/video&gt;&#10;&lt;p&gt;These are the clusters that I selected in a grid&lt;/p&gt;&#10;&lt;figure&gt;&lt;img src="https://user-images.githubusercontent.com/6897215/233811546-36be2cec-eb8c-437a-88d1-595a8602a7ba.jpeg" alt="chosen-clusters-front-and-back" loading="lazy" data-zoomable&gt;&lt;/figure&gt;&lt;p&gt;The selection process was done using &lt;a href="https://github.com/basnijholt/thesis-cover/blob/master/thesis-cover-selection.ipynb" target="_blank" rel="noopener"&gt;this Jupyter notebook&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Finally, I copied the selected clusters into different folders and used the OS&amp;rsquo;s file explorer to manually select a few covers from each cluster.&lt;/p&gt;&#10;&lt;h2 id="final-touches-fine-tuning-colors-and-adding-edition-numbers"&gt;Final Touches: Fine-Tuning Colors and Adding Edition Numbers &lt;a class="anchor" href="#final-touches-fine-tuning-colors-and-adding-edition-numbers" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Then I printed an initial version of my thesis, however, I wasn&amp;rsquo;t really happy with the colors.&lt;/p&gt;&#10;&lt;p&gt;To choose the right color scheme I got help from a non-tech savvy friend who went to art school.&#10;Due to the COVID-19 crisis, we couldn&amp;rsquo;t sit side by side, so I created &lt;a href="https://mybinder.org/v2/gh/basnijholt/thesis-cover/master?filepath=color-picker.ipynb" target="_blank" rel="noopener"&gt;this Binder &amp;ldquo;app&amp;rdquo;&lt;/a&gt; with sliders.&lt;/p&gt;&#10;&lt;video style="width: 100%;max-width: 800px;" controls&gt;&#10; &lt;source src="https://user-images.githubusercontent.com/6897215/233811609-8fa74955-0c51-443e-9a87-115921530b7d.mp4" type="video/mp4"&gt;&#10; Your browser does not support the video tag.&#10;&lt;/video&gt;&#10;&lt;p&gt;Additionally, since all of them were different anyway, I gave them an identifier and added&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;edition n of 120&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;to the back.&lt;/p&gt;&#10;&lt;p&gt;These are a few examples of the final result (click on one of the images to open gallery view)&lt;/p&gt;&#10;&lt;div class="gallery"&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0061.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0061.pickle_hu_8e2f6288e2299f4b.webp" width="350" height="242" alt="Image 1 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0087.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0087.pickle_hu_cfe0a7647b8b4d1a.webp" width="350" height="242" alt="Image 2 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0094.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0094.pickle_hu_4767f3839b1f22bc.webp" width="350" height="242" alt="Image 3 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0119.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0119.pickle_hu_9a20ab2d5318b96f.webp" width="350" height="242" alt="Image 4 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0254.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha__data_learner_0254.pickle_hu_e670a31aca40cc2e.webp" width="350" height="242" alt="Image 5 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha2__data_learner_0175.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-alpha2__data_learner_0175.pickle_hu_236481dab96c15b5.webp" width="350" height="242" alt="Image 6 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-rotation-0-90-move-so-slowly__data_learner_0457.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-rotation-0-90-move-SO-slowly__data_learner_0457.pickle_hu_cc81d6c6a833f7ce.webp" width="350" height="242" alt="Image 7 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-rotation-0-90-move-so-slowly__data_learner_0699.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-rotation-0-90-move-SO-slowly__data_learner_0699.pickle_hu_4744ce916ab208d2.webp" width="350" height="242" alt="Image 8 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-rotation-0-90-move-so-slowly__data_learner_0811.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__gradient-sweep-rotation-0-90-move-SO-slowly__data_learner_0811.pickle_hu_5f6e91d59b7c978e.webp" width="350" height="242" alt="Image 9 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__mu-sweep2__data_learner_0179.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__mu-sweep2__data_learner_0179.pickle_hu_ad9a40d0f92c3c0f.webp" width="350" height="242" alt="Image 10 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__mu-sweep2__data_learner_0254.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__mu-sweep2__data_learner_0254.pickle_hu_2e0e64805618b36d.webp" width="350" height="242" alt="Image 11 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10; &lt;a href="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__mu-sweep2__data_learner_0255.pickle.jpg"&gt;&lt;img src="https://www.nijho.lt/media/albums/basnijholt-thesis-cover/data__mu-sweep2__data_learner_0255.pickle_hu_e7bc31a50788a8cd.webp" width="350" height="242" alt="Image 12 of 12" loading="lazy" data-zoomable&gt;&lt;/a&gt;&#10;&lt;/div&gt;&#10;&#10;&lt;p&gt;A few days later, this was delivered to my doorstep&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://user-images.githubusercontent.com/6897215/233811518-bd5426d6-05bc-4c7a-854a-4c373aa21fcf.jpg" alt="theses-everywhere" loading="lazy" data-zoomable&gt;&#10;&lt;img src="https://user-images.githubusercontent.com/6897215/233811525-645b67a4-a9ea-48ba-a391-4639ed92c19d.jpg" alt="printed" loading="lazy" data-zoomable&gt;&lt;/p&gt;&#10;&lt;p&gt;And finally I mailed them to my Ph.D. committee&lt;/p&gt;&#10;&lt;figure&gt;&lt;img src="https://user-images.githubusercontent.com/6897215/233811533-3b85ff8d-8359-453b-8aa1-ef653629a0f4.jpg" alt="mail" loading="lazy" data-zoomable&gt;&lt;/figure&gt;&lt;p&gt;Now it&amp;rsquo;s time to start preparing for my defense at May 11, 2020 at 15:00!&lt;/p&gt;&#10;&lt;h3 id="notes"&gt;Notes &lt;a class="anchor" href="#notes" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;The data and code to generate all printed covers is available at &lt;a href="https://github.com/basnijholt/thesis-cover" target="_blank" rel="noopener"&gt;basnijholt/thesis-cover&lt;/a&gt; &lt;i class="fab fa-github"&gt;&lt;/i&gt;&lt;/li&gt;&#10;&lt;li&gt;The simulation code that generates the data is available at &lt;a href="https://github.com/basnijholt/spin-orbit-nanowires" target="_blank" rel="noopener"&gt;basnijholt/spin-orbit-nanowires&lt;/a&gt; &lt;i class="fab fa-github"&gt;&lt;/i&gt;&lt;/li&gt;&#10;&lt;li&gt;The video of the Reddit post is generated using &lt;a href="https://gist.github.com/basnijholt/7aaeb7b9c506f830e6791d264a6cf467" target="_blank" rel="noopener"&gt;this code&lt;/a&gt; &lt;i class="fab fa-github"&gt;&lt;/i&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>pfapack</title><link>https://www.nijho.lt/project/pfapack/</link><pubDate>Mon, 02 Mar 2020 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/pfapack/</guid><description/></item><item><title>slurm-usage</title><link>https://www.nijho.lt/project/slurm-usage/</link><pubDate>Tue, 14 Jan 2020 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/slurm-usage/</guid><description/></item><item><title>Adaptive-scheduler</title><link>https://www.nijho.lt/project/adaptive-scheduler/</link><pubDate>Mon, 25 Nov 2019 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/adaptive-scheduler/</guid><description/></item><item><title>Home Assistant</title><link>https://www.nijho.lt/project/home-assistant/</link><pubDate>Mon, 25 Nov 2019 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/home-assistant/</guid><description/></item><item><title>ipynb_git_filters</title><link>https://www.nijho.lt/project/ipynb_git_filters/</link><pubDate>Mon, 25 Nov 2019 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/ipynb_git_filters/</guid><description/></item><item><title>python-aiokef</title><link>https://www.nijho.lt/project/aiokef/</link><pubDate>Mon, 25 Nov 2019 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/aiokef/</guid><description/></item><item><title>python-hpc05</title><link>https://www.nijho.lt/project/hpc05/</link><pubDate>Mon, 25 Nov 2019 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/hpc05/</guid><description/></item><item><title>python-yaml2bib</title><link>https://www.nijho.lt/project/yaml2bib/</link><pubDate>Tue, 12 Mar 2019 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/yaml2bib/</guid><description/></item><item><title>Python-adaptive</title><link>https://www.nijho.lt/project/adaptive/</link><pubDate>Fri, 06 Jul 2018 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/adaptive/</guid><description/></item><item><title>python-instacron</title><link>https://www.nijho.lt/project/instacron/</link><pubDate>Fri, 06 Jul 2018 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/instacron/</guid><description/></item><item><title>Topology in condensed matter course</title><link>https://www.nijho.lt/project/topocondmat/</link><pubDate>Fri, 06 Jul 2018 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/topocondmat/</guid><description/></item><item><title>fileup</title><link>https://www.nijho.lt/project/fileup/</link><pubDate>Fri, 24 Feb 2017 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/fileup/</guid><description/></item><item><title>miflora</title><link>https://www.nijho.lt/project/miflora/</link><pubDate>Thu, 25 Aug 2016 00:00:00 +0000</pubDate><guid isPermaLink="false">/project/miflora/</guid><description/></item></channel></rss>