<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[The comfy chair]]></title><description><![CDATA[The comfy chair]]></description><link>https://roderik.no</link><generator>RSS for Node</generator><lastBuildDate>Sat, 05 Sep 2026 23:37:04 GMT</lastBuildDate><atom:link href="https://roderik.no/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Bazzite ramblings]]></title><description><![CDATA[First of all. Why would you chose to run Bazzite linux? Well, in this day and age obviously because it’s the most complete linux for any LLM and AI agent work you can imagine. Provided you have a supported GPU - everything works out of the box, and i...]]></description><link>https://roderik.no/bazzite-ramblings</link><guid isPermaLink="true">https://roderik.no/bazzite-ramblings</guid><category><![CDATA[bazzite]]></category><category><![CDATA[neomutt]]></category><category><![CDATA[syncmail]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Distrobox]]></category><category><![CDATA[systemd]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Sat, 01 Mar 2025 15:46:54 GMT</pubDate><content:encoded><![CDATA[<p>First of all. Why would you chose to run <strong>Bazzite</strong> linux? Well, in this day and age obviously because it’s the most complete linux for any LLM and AI agent work you can imagine. Provided you have a supported GPU - everything works out of the box, and it’s optimized for gaming. We all know gamers takes their rig more seriously than anyone on the planet.</p>
<p>What’s the down side? The down side can be that it’s not easy to operate <strong>Bazzite</strong> with out pulling down packages and making layers on top of your imutable OS, but it’s far from impossible.</p>
<p>Enter <code>distrobox</code> (pun intended). Why not <code>toolbox</code> ? <code>toolbox</code>has become very complex, and it still lacks features that <code>distrobox</code> rocks. <code>distrobox</code> on the other hand is dirt simple (everything is relative) shell scripts that does it’s magic and never complains. No, really. <code>distrobox</code> is very clever and, although I, like some many else, has a devcrush on <code>golang</code>, the <code>distrobox</code> devs made the right choice.</p>
<p>A typical <code>distrobox</code> file might look something like this:</p>
<pre><code class="lang-ini"><span class="hljs-section">[fedora41]</span> <span class="hljs-comment"># also inline comments are supported</span>
<span class="hljs-attr">additional_packages</span>=<span class="hljs-string">"zsh fzf neovim golang cargo glib2-devel pango-devel cairo-gobject-devel
atk-devel javascriptcoregtk4.1-devel libsoup3-devel gtk3-devel webkit2gtk4.1-devel 
python3-pip pciutils tmux neomutt curl isync msmtp pass ca-certificates 
gettext lynx abook chronie qutebrowser"</span>
<span class="hljs-attr">image</span>=registry.fedoraproject.org/fedora-toolbox:latest
<span class="hljs-attr">home</span>=/home/&lt;user&gt;/distroboxhome/
<span class="hljs-attr">init</span>=<span class="hljs-literal">false</span>
<span class="hljs-attr">start_now</span>=<span class="hljs-literal">false</span>
<span class="hljs-attr">nvidia</span>=<span class="hljs-literal">true</span>
<span class="hljs-attr">pull</span>=<span class="hljs-literal">true</span>
<span class="hljs-attr">root</span>=<span class="hljs-literal">false</span>
<span class="hljs-attr">replace</span>=<span class="hljs-literal">false</span>
</code></pre>
<p>The really big difference, for me, is the ease of which you can switch to a custom <code>$HOME</code> with <code>distrobox</code>.</p>
<pre><code class="lang-bash">$ distrobox assemble create --file distrobox.ini 
$ distrobox enter fedora41
</code></pre>
<p>That’s it. Now you have created and entered into a <code>distrobox</code> container (<code>podman</code>) in which you have installed all the <code>additional_packages</code> listed in the <code>.ini</code> file. Smooth AF.</p>
<p>So what’s the problem? The <em>problem</em> is that I have a mail client system consisting of <code>neomutt</code> configured with <code>mutt-wizard</code> relying on <code>mbsync</code> and <code>notmuch</code>. It’s a marginal problem, but for me it is a problem because I’ve decided that I will live my life inside a <code>distrobox</code> and never install anything that is not already present in <strong>Bazzite</strong> proper.</p>
<p>This in turn leads to <a target="_blank" href="https://github.com/roderik333/syncmail">https://github.com/roderik333/syncmail</a></p>
<p>..and I’ll try to explain why.</p>
<p>I can’t just install a <code>init</code>system in my <code>distrobox</code></p>
<pre><code class="lang-bash">$ distrobox create --<span class="hljs-built_in">help</span>
.
.
-- init/I       use init system (like systemd) inside the container.
                this will make hosts processes not visible from within the container.
.
.
</code></pre>
<p>And as explained in the <strong>README.md</strong> of <code>syncmail</code> running <code>.timer</code> on the <code>systemd</code> of <strong>Bazzite</strong> proper didn’t work very well.</p>
<p>HOWEVER! I still might do this if I manage to reform my workflow.. because I could run my mail in a separate <code>distrobox</code> that has a init system, and the rest of everything in a different <code>distrobox</code>.</p>
<p>I think I would then need a multiplexer in <strong>Bazzite</strong> terminal and a separate multiplexer for each <code>distrobox</code> which I fear might become a tad confusing.</p>
<p><strong>Ghostty</strong> might also be an alternative. Haven’t tried it but it’s meant to send <code>tmux</code> and <code>zellij</code> in oblivion. But I can not use <code>rpm-ostree</code> to just install <strong>Ghosttty</strong> that would defat the whole purpose of this .. kinda moronic endeavour, anyways. <strong>Bazzite</strong> supports <code>appimages</code> out of the box. So there’s that.</p>
<p>‘till next time</p>
]]></content:encoded></item><item><title><![CDATA[podman and Open WebUI]]></title><description><![CDATA[There are numerous tutorials both on youtube and on blog platforms that shows how to get Open WebUI up and running on docker.
Not too many mentions of podman, so here goes.
$ podman create -p 127.0.0.1:3000:8080 --network=pasta:-T,11434 \
--add-host=...]]></description><link>https://roderik.no/podman-and-open-webui</link><guid isPermaLink="true">https://roderik.no/podman-and-open-webui</guid><category><![CDATA[podman]]></category><category><![CDATA[open-webui]]></category><category><![CDATA[ollama]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Sun, 01 Dec 2024 13:43:09 GMT</pubDate><content:encoded><![CDATA[<p>There are numerous tutorials both on youtube and on blog platforms that shows how to get Open WebUI up and running on docker.</p>
<p>Not too many mentions of podman, so here goes.</p>
<pre><code class="lang-bash">$ podman create -p 127.0.0.1:3000:8080 --network=pasta:-T,11434 \
--add-host=localhost:127.0.0.1 \
--env <span class="hljs-string">'OLLAMA_BASE_URL=http://localhost:11434'</span> \
--env <span class="hljs-string">'ANONYMIZED_TELEMETRY=False'</span> \
-v open-webui:/app/backend/data \
--label io.containers.autoupdate=registry \
--name open-webui ghcr.io/open-webui/open-webui:main
</code></pre>
<p>This will allow you to access Open WebUI on http://localhost:3000.</p>
<p>Remember to pull the image ghcr.io/open-webui/open-webui</p>
<p>‘till next time.</p>
]]></content:encoded></item><item><title><![CDATA[780M, Ollama, oh dear]]></title><description><![CDATA[So, shiny new lenovo with a 8840HS CPU rocking the 780M (A)GPU

💡
Installing Ollama was straight forward. Pulling models, also very easy. Understanding why the (A)GPU was ignored - a bit more cumbersome.


This was performed on a lenovo P14s 5Gen AM...]]></description><link>https://roderik.no/780m-ollama-oh-dear</link><guid isPermaLink="true">https://roderik.no/780m-ollama-oh-dear</guid><category><![CDATA[RDNA]]></category><category><![CDATA[apu]]></category><category><![CDATA[ollama]]></category><category><![CDATA[lmstudio]]></category><category><![CDATA[GPU]]></category><category><![CDATA[ROCm]]></category><category><![CDATA[AI]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[aitools]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Sat, 30 Nov 2024 13:58:14 GMT</pubDate><content:encoded><![CDATA[<p>So, shiny new lenovo with a 8840HS CPU rocking the 780M (A)GPU</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Installing <strong>Ollama</strong> was straight forward. Pulling models, also very easy. Understanding why the (A)<strong>GPU</strong> was ignored - a bit more cumbersome.</div>
</div>

<p>This was performed on a lenovo P14s 5Gen AMD 8840HS with 32GB memory running Fedora Workstation 41</p>
<p>Let’s start with the obvious stuff. The latest <code>GPU</code> firmware must be installed and the latest version of <strong>ollama</strong> must also be installed. As of this writing it is <strong>ollama</strong> 0.4.6</p>
<p>Then you may, or you may not, want to up the dedicated amount of “VRAM” your <code>(A)GPU</code> is allowed to use. My, albeit limited, experience is that leaving it at <strong>AUTOMATIC</strong> will send 50% of the processing to the <code>CPU</code> and 50% to the <code>(A)GPU</code> when working with models around ~<code>4GB</code> in size. Performance wise this isn’t too bad on my system, however setting BIOS to allocate <code>8GB will</code> allow for ~<code>8GB</code> models to use the <code>(A)GPU</code> only..(and then also smaller models naturally).. which is faster, but wastes memory in most scenarios. I also noticed some instabilities with <code>8GB VRAM</code> allocated, but I did not care to investigate it further.</p>
<p>I haven’t tried larger models with command line <strong>ollama</strong>.. i did try <code>14GB</code> models using <a target="_blank" href="https://lmstudio.ai/">LM Studio</a>. It worked, but it crashed my gnome session after a while and besides, I don’t think I like the <a target="_blank" href="https://lmstudio.ai/terms">LM Studios terms</a> so I removed it from my system.</p>
<p>If you have followed along this far you’ve realized that “that guy is full of s**t.. this doesn’t work!” and you’d be right. I have not come to the most important part yet.</p>
<p>If you do -</p>
<pre><code class="lang-bash">$ journalctl -fu ollama --no-pager
</code></pre>
<p>you will see loglines indicating that <strong>ollama</strong> does not know about your <code>(A)GPU</code>.. even though <strong>ollama</strong> clearly stated that it was present when you installed <strong>ollama</strong>. So we need to remedy that.</p>
<p>I guess there are multiple ways to perform this next step, but I chose the path of least resistance.</p>
<pre><code class="lang-bash">$ sudo vim /etc/systemd/system/ollama.service
<span class="hljs-comment"># insert the following in to the [Service] section. Yes, there is already</span>
<span class="hljs-comment"># a variable named Environment, we will add one more.</span>
Environment=<span class="hljs-string">"HSA_OVERRIDE_GFX_VERSION=11.0.2"</span>

<span class="hljs-comment"># save the file and exit</span>
$ sudo systemctl daemon-reload
$ sudo systectl restart ollama.service
</code></pre>
<p>Finding the correct <em>HSA_OVERRIDE_GFX_VERSION"</em> was pain in the proverbial butt! The only place I’ve found it specified is here <a target="_blank" href="https://discuss.linuxcontainers.org/t/rocm-and-pytorch-on-amd-apu-or-gpu-ai/19743">https://discuss.linuxcontainers.org/t/rocm-and-pytorch-on-amd-apu-or-gpu-ai/19743</a></p>
<p>I’ll admit that my google foo is not as strong as it once was, but I blame google and not my self (I had to give up on duckduck.. )</p>
<ul>
<li><p>for GCN 5th gen based GPUs and APUs HSA_OVERRIDE_GFX_VERSION=9.0.0</p>
</li>
<li><p>for RDNA 1 based GPUs and APUs HSA_OVERRIDE_GFX_VERSION=10.1.0</p>
</li>
<li><p>for RDNA 2 based GPUs and APUs HSA_OVERRIDE_GFX_VERSION=10.3.0</p>
</li>
<li><p>for RDNA 3 based GPUs and APUs HSA_OVERRIDE_GFX_VERSION=11.0.0</p>
</li>
</ul>
<p>I mucked about with 10.3.0, which was the only reference I found to <em>GFX_VERSION</em>, for several hours until I stumbled upon this guy: <a target="_blank" href="https://discuss.linuxcontainers.org/u/qkiel/summary">qkiel</a> So, thank you <strong>qkiel,</strong> you saved my sanity.</p>
<p>So, there’s that.</p>
<p>I leave you with this</p>
<pre><code class="lang-bash">zsh&gt; ollama run llama3.2
&gt;&gt;&gt; Why should I blog about llama?
Blogging about llamas can be a unique and fascinating niche. Here are some reasons why you might consider starting a blog about llamas:

1. **Growing interest <span class="hljs-keyword">in</span> alternative pets**: Llamas have become increasingly popular as exotic pets, and people are willing to learn more 
about their care, behavior, and needs.
2. **Unique perspective**: By focusing on llamas, you can offer a fresh and distinct voice <span class="hljs-keyword">in</span> the world of pet blogging or wildlife writing.
3. **Educational value**: Llamas are intelligent, social animals that require specific care and attention. A blog about llamas can provide 
valuable information <span class="hljs-keyword">for</span> potential owners, breeders, or anyone interested <span class="hljs-keyword">in</span> these amazing creatures.
4. **Community building**: You can connect with a like-minded community of llama enthusiasts, share experiences, and build relationships 
with people who share your passion <span class="hljs-keyword">for</span> these animals.
5. **Creative expression**: Writing about llamas can be a creative outlet, allowing you to express yourself through storytelling, poetry, or 
other forms of writing.
6. **Monetization opportunities**: If you have a large and engaged audience, you may be able to monetize your blog through advertising, 
affiliate marketing, or sponsored content related to llama-related products or services.
7. **Personal growth**: Learning about llamas can be a fun and rewarding experience that helps you develop new skills, such as animal care, 
research, or writing.

Some potential blog post ideas to get you started:

* <span class="hljs-string">"10 Fun Facts About Llamas You Never Knew"</span>
* <span class="hljs-string">"A Beginner's Guide to Keeping Llamas as Pets"</span>
* <span class="hljs-string">"The Benefits of Llama Therapy and How It Can Help People"</span>
* <span class="hljs-string">"Llama Behavior: Understanding Their Body Language and Social Structure"</span>
* <span class="hljs-string">"Llama Care and Nutrition: What You Need to Know"</span>

Remember, starting a blog is just the first step. To make it successful, focus on creating high-quality content, engaging with your 
audience, and continuously learning about llamas to share your knowledge with others.
</code></pre>
<p>‘till next time.</p>
]]></content:encoded></item><item><title><![CDATA[Curing screen flicker]]></title><description><![CDATA[I just got hold of a brand new Lenovo p14s gen5 Ryzen 7 pro 8840HS with integrated 780 graphics.
At first I thought .. I don’t know what I thought really… anyways, the screen flickered at random intervals and rather infrequently too.
A quick search f...]]></description><link>https://roderik.no/curing-screen-flicker</link><guid isPermaLink="true">https://roderik.no/curing-screen-flicker</guid><category><![CDATA[lenovo thinkpad]]></category><category><![CDATA[Flicker ]]></category><category><![CDATA[amd]]></category><category><![CDATA[GPU]]></category><category><![CDATA[grub2]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Fri, 22 Nov 2024 10:43:06 GMT</pubDate><content:encoded><![CDATA[<p>I just got hold of a brand new Lenovo p14s gen5 Ryzen 7 pro 8840HS with integrated 780 graphics.</p>
<p>At first I thought .. I don’t know what I thought really… anyways, the screen flickered at random intervals and rather infrequently too.</p>
<p>A quick search found a solution that works for me with my current configuration</p>
<pre><code class="lang-bash">$ uname -a 
Linux toblakai 6.11.8-300.fc41.x86_64 <span class="hljs-comment">#1 SMP PREEMPT_DYNAMIC Thu Nov 14 20:37:39 UTC 2024 x86_64 GNU/Linux</span>
</code></pre>
<pre><code class="lang-bash">$ lspci -n -n -k | grep -A 2 -e VGA -e 3
.
.
c4:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Phoenix3 [1002:1900] (rev d2)                                    
        Subsystem: Lenovo Device [17aa:50ef]                                                                                                      
        Kernel driver <span class="hljs-keyword">in</span> use: amdgpu
</code></pre>
<p>The flickering might or might not have started after I ran <code>fwupdmgr update</code> which updated the Phoenix firmware..</p>
<p>.. in either case, it seems to have been solved by editing <code>/etc/default/grub</code> adding <code>amdgpu.dcdebugmask=0x10</code> to the end of the <code>GRUB_CMDLINE_LINUX</code> line and then</p>
<pre><code class="lang-bash">$ grub2-mkconfig -o /boot/grub2/grub.cfg
$ reboot
</code></pre>
<p>As mentioned, this works for me. Supposedly it will hurt the battery life of the machine, but not by much.. and there are several reports of people that wasn’t helped by this <em>fix</em> .. so your mileage may vary.</p>
<p>‘till next time.</p>
]]></content:encoded></item><item><title><![CDATA[AstroNvim custom filetype and Icon]]></title><description><![CDATA[https://gist.github.com/roderik333/d910f0870b4e23b887210fdd6971b365
https://gist.github.com/roderik333/d910f0870b4e23b887210fdd6971b365]]></description><link>https://roderik.no/astronvim-custom-filetype-and-icon</link><guid isPermaLink="true">https://roderik.no/astronvim-custom-filetype-and-icon</guid><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Wed, 30 Oct 2024 08:59:16 GMT</pubDate><content:encoded><![CDATA[<p><a target="_blank" href="https://gist.github.com/roderik333/d910f0870b4e23b887210fdd6971b365">https://gist.github.com/roderik333/d910f0870b4e23b887210fdd6971b365</a></p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="d910f0870b4e23b887210fdd6971b365"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/roderik333/d910f0870b4e23b887210fdd6971b365" class="embed-card">https://gist.github.com/roderik333/d910f0870b4e23b887210fdd6971b365</a></div>]]></content:encoded></item><item><title><![CDATA[Pandas dataframe to django Model]]></title><description><![CDATA[So, pandas has a lot going for it. Yes I know. Those who are all about speed use something else. I don’t. I really like pandas, and I can wait a millisecond for my process to complete.
Pandas function, .to_sql, which in combination with SQLAlchemy is...]]></description><link>https://roderik.no/pandas-dataframe-to-django-model</link><guid isPermaLink="true">https://roderik.no/pandas-dataframe-to-django-model</guid><category><![CDATA[Python 3]]></category><category><![CDATA[Django]]></category><category><![CDATA[pandas]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Fri, 20 Sep 2024 06:58:38 GMT</pubDate><content:encoded><![CDATA[<p>So, pandas has a lot going for it. Yes I know. Those who are all about speed use something else. I don’t. I really like pandas, and I can wait a millisecond for my process to complete.</p>
<p>Pandas function, .to_sql, which in combination with SQLAlchemy is really really cool. I had use for something that worked with django, so I whipped up this contraption. No, it’s no where near .to_sql, but it does the job. For me. In this instance.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> pandas <span class="hljs-keyword">as</span> pd
<span class="hljs-keyword">import</span> numpy <span class="hljs-keyword">as</span> np
<span class="hljs-keyword">from</span> django.db <span class="hljs-keyword">import</span> models

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">dataframe_to_django</span>(<span class="hljs-params">
    df: Series | Any,
    model: type[models.Model],
    dbmapping: dict[str, str | None],
    convmapping: dict[str, Any],
    replace: bool = False,
</span>) -&gt; <span class="hljs-keyword">None</span>:</span>
    <span class="hljs-string">"""Uses bulk_create to insert data in table.

    KeywordArguments:
      df           --  the dataframe.
      model        --  the django table in to which the dataframe should be inserted.
      dbmapping    --  mapping from dataframe name to db name. Typically {"ProductName": "product_name", "SkuDescription": None, ...}
      convmapping  --  converters for spesific fields. Typically, {"StartDate": pd.to_datetime, ...} .
      replace      --  bool False|True. If set to True it will wipe the table before import.
    """</span>

    <span class="hljs-keyword">if</span> replace:
        model.objects.all().delete()

    df.replace([np.nan], [<span class="hljs-string">"N/A"</span>], inplace=<span class="hljs-literal">True</span>)
    <span class="hljs-keyword">for</span> key <span class="hljs-keyword">in</span> convmapping:
        df[key] = convmapping[key](df[key])

    model_list: list[models.Model] = [
        model(**{dbmapping[k]: v <span class="hljs-keyword">for</span> k, v <span class="hljs-keyword">in</span> x.items() <span class="hljs-keyword">if</span> dbmapping[k] <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>}) <span class="hljs-keyword">for</span> x <span class="hljs-keyword">in</span> df.to_dict(<span class="hljs-string">"records"</span>)
    ]
    model.objects.bulk_create(model_list)
</code></pre>
<p>The link to the gist is https://gist.github.com/roderik333/0343f54af1e27dcbd50affb19919ac11</p>
<p>until next time..</p>
]]></content:encoded></item><item><title><![CDATA[Podman: Postgresql with pgadmin]]></title><description><![CDATA[This is a quick one.
I needed to replace the sqlite3 database I was working with. sqlite3 is amazing, but has it's limits.
Naturally I turned to postgres:16
$ podman pod create --name postgre-sql -p 9876:80

This creates a pod with an opening for pga...]]></description><link>https://roderik.no/podman-postgresql-with-pgadmin</link><guid isPermaLink="true">https://roderik.no/podman-postgresql-with-pgadmin</guid><category><![CDATA[podman]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[pgAdmin]]></category><category><![CDATA[kube]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Mon, 08 Jul 2024 08:26:02 GMT</pubDate><content:encoded><![CDATA[<p>This is a quick one.</p>
<p>I needed to replace the sqlite3 database I was working with. sqlite3 is amazing, but has it's limits.</p>
<p>Naturally I turned to postgres:16</p>
<pre><code class="lang-bash">$ podman pod create --name postgre-sql -p 9876:80
</code></pre>
<p>This creates a pod with an opening for pgadmin on port 9876</p>
<pre><code class="lang-bash">$ podman run --pod postgre-sql -e <span class="hljs-string">'PGADMIN_DEFAULT_EMAIL=syouradmin@yourdomain.com'</span> -e <span class="hljs-string">'PGADMIN_DEFAULT_PASSWORD=passWoRd'</span> --name pgadmin -d docker.io/dpage/pgadmin4:latest
</code></pre>
<p>This creates a container with pgadmin running in the postgre-sql pod.</p>
<p>Now, create a volume</p>
<pre><code class="lang-bash">$ podman volume create postgres-volume
</code></pre>
<p>And use that volume when setting up the final container</p>
<pre><code class="lang-bash">$ podman run --name mypgdb --pod postgre-sql -d -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=passWoRd -v postgres-volume:/var/lib/postgresql/data:rw,z docker.io/library/postgres:16
</code></pre>
<p>Done.</p>
<p>Now, connect to your pgadmin instance on http://localhost:9876</p>
<ul>
<li><p>Add new server</p>
<ul>
<li><p>General</p>
<ul>
<li>name == mypgdb (or what ever name you gave the database when created)</li>
</ul>
</li>
<li><p>Connection</p>
<ul>
<li><p>Hostname / address == 0.0.0.0</p>
</li>
<li><p>Username == &lt;your user&gt;</p>
</li>
<li><p>Password == &lt;your password&gt;</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p>Connect</p>
</li>
</ul>
<p>You should now be connected to your postgres instance.</p>
<p>The Kube spec:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># Save the output of this file and use kubectl create -f to import</span>
<span class="hljs-comment"># it into Kubernetes.</span>
<span class="hljs-comment">#</span>
<span class="hljs-comment"># Created with podman-5.1.1</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Pod</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">creationTimestamp:</span> <span class="hljs-literal">null</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">postgre-sql</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">postgre-sql</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">containers:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">env:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">PGADMIN_DEFAULT_EMAIL</span>
      <span class="hljs-attr">value:</span> <span class="hljs-string">&lt;your.email&gt;@&lt;your.domain&gt;</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">PGADMIN_DEFAULT_PASSWORD</span>
      <span class="hljs-attr">value:</span> <span class="hljs-string">passWoRd</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">docker.io/dpage/pgadmin4:latest</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">pgadmin</span>
    <span class="hljs-attr">ports:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">80</span>
      <span class="hljs-attr">hostPort:</span> <span class="hljs-number">9876</span>
    <span class="hljs-attr">securityContext:</span> {}
    <span class="hljs-attr">volumeMounts:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/var/lib/pgadmin</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">fe79dca434a958057fa493fb87879ccc2fdc3a73167a7940ff8a382f17ef548b-pvc</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">args:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">postgres</span>
    <span class="hljs-attr">env:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">POSTGRES_PASSWORD</span>
      <span class="hljs-attr">value:</span> <span class="hljs-string">passWoRd</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">POSTGRES_USER</span>
      <span class="hljs-attr">value:</span> <span class="hljs-string">admin</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">docker.io/library/postgres:16</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">mypgdb</span>
    <span class="hljs-attr">volumeMounts:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/var/lib/postgresql/data</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">postgre-sql-volume-pvc</span>
  <span class="hljs-attr">volumes:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">fe79dca434a958057fa493fb87879ccc2fdc3a73167a7940ff8a382f17ef548b-pvc</span>
    <span class="hljs-attr">persistentVolumeClaim:</span>
      <span class="hljs-attr">claimName:</span> <span class="hljs-string">fe79dca434a958057fa493fb87879ccc2fdc3a73167a7940ff8a382f17ef548b</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">postgre-sql-volume-pvc</span>
    <span class="hljs-attr">persistentVolumeClaim:</span>
      <span class="hljs-attr">claimName:</span> <span class="hljs-string">postgre-sql-volume</span>
</code></pre>
<p>What the keen eyed observer might have noticed is that you cant reach the postgres db from localhost... or any other host for that matter. This is by design of course. Add an application container to the pod and the application container has full access to the database. However, this is not what I want so we have to do a minor update to the kube.yaml file.</p>
<p>In the postgres section of the yaml file, under name: mypgdb add</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">mypgdb</span>
<span class="hljs-attr">ports:</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">5432</span>
  <span class="hljs-attr">hostPort:</span> <span class="hljs-number">5432</span>
</code></pre>
<p>Save and re run with</p>
<pre><code class="lang-bash">$ podman kube play --replace kube.yaml
</code></pre>
<p>this exposes the db to my ad-hoc scripts and applications on localhost. Convenient!</p>
<p>anyways.. 'till next time.</p>
]]></content:encoded></item><item><title><![CDATA[Python settings revisited]]></title><description><![CDATA[So, earlier I mucked about with pydantic, and then some plain dataclasses for the sake of making a settings object. Why should you? Well.. it's darn snazzy! And further more it helps in that you can compartmentalize your settings as you app grows.
I ...]]></description><link>https://roderik.no/python-settings-revisited</link><guid isPermaLink="true">https://roderik.no/python-settings-revisited</guid><category><![CDATA[dataclass]]></category><category><![CDATA[Python]]></category><category><![CDATA[Django]]></category><category><![CDATA[settings]]></category><category><![CDATA[configuration]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Thu, 02 May 2024 10:00:37 GMT</pubDate><content:encoded><![CDATA[<p>So, earlier I mucked about with pydantic, and then some plain dataclasses for the sake of making a settings object. Why should you? Well.. it's darn snazzy! And further more it helps in that you can compartmentalize your settings as you app grows.</p>
<p>I outlined how you could use dotenv to load secrets into your environment and populate a settings object. This time I'm going to expand on the settings object, and I will show how to add the settings object to django.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">In a django application your environment is typically defined either in your kube.yaml file or docker_compose.yaml file.. The first is Podman/kubernetes and the second is podman/docker. And if you are not using containers.. you really should.</div>
</div>

<p>Below is a non-typical django settings structure. Typically, when you start a project and add an application the application will contain a <em>settings.py</em> file. This is fine, for the most part. However - I tend to build monoliths, this after years and years of micro services. I've learned my lesson! Monoliths it is (I do of course also use micro services, only they are not so micro any more). Monoliths tend to acquire a lot of applications. Mine does anyway, and therefore I also tend to structure my settings in the way shown below.</p>
<pre><code class="lang-bash">&lt;project&gt;/&lt;app&gt;/settings
├── apps
│   ├── bcentral_settings.py
│   ├── some_settings.py
│   ├── __init__.py
│   ├── sso_settings.py
|   |── logging_settings.py
│   └── other_settings.py
├── base.py
└── __init__.py
</code></pre>
<p><em>base.py</em> is more or less the just the <em>settings.py</em> file. The <em>__init__.py</em> file looks like this:</p>
<pre><code class="lang-bash">$ cat __init__.py
from .base import *
from bcentral_settings.py import *
from .apps.some_settings import *
from .apps.sso_settings import *
from .apps.logging_settings import *
from .apps.other_settings import *
</code></pre>
<p>This means that everything(yeah...) defined in these files will be read into the <em>settings</em> namespace. For example, <em>logging_settings.py</em> contains a dictionary</p>
<pre><code class="lang-python">LOGGING = {
    <span class="hljs-string">"version"</span>: <span class="hljs-number">1</span>, 
    ....
}
</code></pre>
<p>I just prefer to have the logging in a separate configuration file. There's no magic here.. other than the magic that django introduces by automagically configuring the logger when it encounters <strong>LOGGING</strong>.. still. It's not like it's Galtworth or something.</p>
<p>We are just going to look at <em>bcentral_settings.py</em></p>
<p>Let's start with some code.</p>
<pre><code class="lang-python"><span class="hljs-string">"""A settings class."""</span>
<span class="hljs-keyword">import</span> os
<span class="hljs-keyword">from</span> dataclasses <span class="hljs-keyword">import</span> dataclass, field
<span class="hljs-keyword">from</span> django.core.exceptions <span class="hljs-keyword">import</span> ValidationError

__all__ = [<span class="hljs-string">"BCENTRAL"</span>]

PREFIX = <span class="hljs-string">"BCENTRAL_"</span>  <span class="hljs-comment"># the environment variables are prefixed</span>

<span class="hljs-meta">@dataclass</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BCentralSettings</span>:</span>
    CLIENT_ID: str = field(default=<span class="hljs-string">""</span>)
    CLIENT_SECRET: str = field(default=<span class="hljs-string">""</span>)
    TENANT_ID: str = field(default=<span class="hljs-string">""</span>)
    API_URL: str = field(default=<span class="hljs-string">""</span>)
    AUTH_ENDPOINT: str = <span class="hljs-string">"https://login.microsoftonline.com//oauth2/v2.0/token"</span>
    COMPANY_UUID: str = field(default=<span class="hljs-string">""</span>)
    SCOPE: list[str] = field(default_factory=<span class="hljs-keyword">lambda</span>: [
        <span class="hljs-string">"https://api.businesscentral.dynamics.com/.default"</span>
        ])
</code></pre>
<p>So far pretty normal. We are going to add a <em>__post_init__</em> magic method to the class. In this <em>__post_init__</em> we will build the mechanics for populating the class with environment variables, and we will add validation mechanics. The validation is no different from what you'd normally do in django (or any other framework / program). <a target="_blank" href="https://docs.djangoproject.com/en/5.0/ref/validators/">https://docs.djangoproject.com/en/5.0/ref/validators/</a></p>
<pre><code class="lang-python">    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__post_init__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-keyword">for</span> name, _field <span class="hljs-keyword">in</span> self.__dataclass_fields__.items():
            <span class="hljs-keyword">if</span> match := os.getenv(<span class="hljs-string">f"<span class="hljs-subst">{PREFIX}</span><span class="hljs-subst">{name}</span>"</span>):
                setattr(self, name, match)
            <span class="hljs-keyword">if</span> method := getattr(self, <span class="hljs-string">f"validate_<span class="hljs-subst">{name}</span>"</span>, <span class="hljs-literal">None</span>):
                setattr(self, name, method(getattr(self, name), field=_field))
</code></pre>
<p>This code might warrant some explanation. In this implementation of <em>__post_init__</em> we are looping over all the fields in the dataclass and can do whatever we want with them. First, we try to match an environment variable to a name in the dataclass. I tend to keep one flat file of environment variables and distinguish between them I use a prefix that tie into the settings object. In this case <strong>PREFIX = "BCENTRAL_"</strong></p>
<p>The second <em>if</em> statement looks for a metod named <em>validate_NAME</em> in the class.</p>
<pre><code class="lang-python">    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">validate_CLIENT_ID</span>(<span class="hljs-params">self, value: str, field=field</span>) -&gt; str:</span>
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> value:
            <span class="hljs-keyword">raise</span> ValidationError(<span class="hljs-string">f"<span class="hljs-subst">{field.name}</span> is required"</span>)
        <span class="hljs-keyword">return</span> value
</code></pre>
<p>And so on and so forth. You could also make sure <em>value</em> is a valid UUID, has fins and lives in the Gobi desert.. if that's your thing.</p>
<p>After all this, make an instance of your object</p>
<pre><code class="lang-python">BCENTRAL = BCentralSettings()
</code></pre>
<p>Do this at the end of <em>bcentral_settings.py</em>. As you might have noticed <em>__all__ = ["BCENTRAL"]</em> is used. This ensures that <strong>settings</strong> is not polluted with variables from our file.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Note: <em><strong>all</strong></em> affects the <strong>from &lt;module&gt; import * </strong>behavior only<em>. </em>Members that are not mentioned in<em> <strong>all</strong> are</em> still accessible from outside the module and can be imported with <strong>from &lt;module&gt; import &lt;name&gt;</strong>.</div>
</div>

<p>Access your settings:</p>
<pre><code class="lang-python">$ python &lt;project&gt;/manage.py shell
In [<span class="hljs-number">1</span>]: <span class="hljs-keyword">from</span> django.conf <span class="hljs-keyword">import</span> settings
In [<span class="hljs-number">2</span>]: settings.BCENTRAL.CLIENT_ID
<span class="hljs-string">'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'</span>
In [<span class="hljs-number">3</span>]: settings.PREFIX
.
.
AttributeError: <span class="hljs-string">'Settings'</span> object has no attribute <span class="hljs-string">'PREFIX'</span>
</code></pre>
<p>That's about it. Easy configuration management for, in this instance, django.</p>
<p>Until next time..</p>
]]></content:encoded></item><item><title><![CDATA[Nextcloud on Cloudflare Part 2]]></title><description><![CDATA[Some time back I made a blog post about Nextcloud and Cloudflare tunnels
I never got around to make carddav and caldav work.. it slipped my mind at the time. Today, I got annoyed with my self. Local Thunderbird simply did not have my contacts! By Odi...]]></description><link>https://roderik.no/nextcloud-on-cloudflare-part-2</link><guid isPermaLink="true">https://roderik.no/nextcloud-on-cloudflare-part-2</guid><category><![CDATA[carddav]]></category><category><![CDATA[caldav]]></category><category><![CDATA[Nextcloud]]></category><category><![CDATA[cloudflare]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Thu, 11 Apr 2024 10:46:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712832541362/9a88f39e-9326-4fa2-8645-0e127555bbdc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Some time back I made a blog post about <a target="_blank" href="https://roderik.no/nextcloud-and-cloudflare-tunnels">Nextcloud and Cloudflare tunnels</a></p>
<p>I never got around to make carddav and caldav work.. it slipped my mind at the time. Today, I got annoyed with my self. Local Thunderbird simply did not have my contacts! By Odin's sweaty teats, something had to be done!</p>
<p>First, I must assume that you have followed the previous blog post to the letter and are running Nextcloud in a fancy podman pod.</p>
<pre><code class="lang-bash">$ ls -F
containers/ other/ stuff/ thats/ yours/ and/ yours/ alone.mp3
$ <span class="hljs-built_in">cd</span> containers/nextcloud/data/html
$ vim .htaccess
</code></pre>
<p><a target="_blank" href="https://www.vim.org/">vim</a> is not the important part here. Use the editor of your choice.</p>
<p>In .htaccess, navigate to the first(!!) section that starts with</p>
<pre><code class="lang-apache"><span class="hljs-section">&lt;IfModule mod_rewrite.c&gt;</span>
  <span class="hljs-attribute"><span class="hljs-nomarkup">RewriteEngine</span></span> <span class="hljs-literal">on</span>                                                                                                                                                           
  <span class="hljs-attribute"><span class="hljs-nomarkup">RewriteCond</span></span> <span class="hljs-variable">%{HTTP_USER_AGENT}</span> DavClnt
  .
  .
<span class="hljs-section">&lt;/IfModule&gt;</span>
</code></pre>
<p>Replace the entire section with the following</p>
<pre><code class="lang-apache"><span class="hljs-section">&lt;IfModule mod_rewrite.c&gt;</span>                                                                                                                                                     
  <span class="hljs-attribute"><span class="hljs-nomarkup">RewriteEngine</span></span> <span class="hljs-literal">on</span>                                                                                                                                                           
  <span class="hljs-attribute"><span class="hljs-nomarkup">RewriteCond</span></span> <span class="hljs-variable">%{HTTP_USER_AGENT}</span> DavClnt                                                                                                                                     
  <span class="hljs-attribute"><span class="hljs-nomarkup">RewriteRule</span></span> ^$ /remote.php/webdav/<span class="hljs-meta"> [L,R=302]</span>
  <span class="hljs-attribute"><span class="hljs-nomarkup">RewriteRule</span></span> .* -<span class="hljs-meta"> [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]</span>
  <span class="hljs-attribute"><span class="hljs-nomarkup">RewriteRule</span></span> ^\.well-known/carddav /remote.php/dav/<span class="hljs-meta"> [R=301,L]</span>
  <span class="hljs-attribute"><span class="hljs-nomarkup">RewriteRule</span></span> ^\.well-known/caldav /remote.php/dav/<span class="hljs-meta"> [R=301,L]</span>
  <span class="hljs-attribute"><span class="hljs-nomarkup">RewriteRule</span></span> ^\.well-known/webfinger /index.php/.well-known/webfinger<span class="hljs-meta"> [R=301,L]</span>
  <span class="hljs-attribute"><span class="hljs-nomarkup">RewriteRule</span></span> ^\.well-known/nodeinfo /index.php/.well-known/nodeinfo<span class="hljs-meta"> [R=301,L]</span>
  <span class="hljs-attribute"><span class="hljs-nomarkup">RewriteRule</span></span> ^remote/(.*) remote.php<span class="hljs-meta"> [QSA,L]</span>
  <span class="hljs-attribute"><span class="hljs-nomarkup">RewriteRule</span></span> ^(?:build|tests|config|lib|<span class="hljs-number">3</span>rdparty|templates)/.* -<span class="hljs-meta"> [R=404,L]</span>
  <span class="hljs-attribute"><span class="hljs-nomarkup">RewriteRule</span></span> ^\.well-known/(?!acme-challenge|pki-validation) /index.php<span class="hljs-meta"> [QSA,L]</span>
  <span class="hljs-attribute"><span class="hljs-nomarkup">RewriteRule</span></span> ^ocm-provider/?$ index.php<span class="hljs-meta"> [QSA,L]</span>
  <span class="hljs-attribute"><span class="hljs-nomarkup">RewriteRule</span></span> ^(?:\.(?!well-known)|autotest|occ|issue|indie|db_|console).* -<span class="hljs-meta"> [R=404,L]</span>
<span class="hljs-section">&lt;/IfModule&gt;</span>
</code></pre>
<p>Remember to blatantly ignore the rather stern warning found under this structure that states: "<strong>DO NOT CHANGE ANYTHING ABOVE THIS LINE</strong>"</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">👹</div>
<div data-node-type="callout-text">life on the edge is good!</div>
</div>

<p>What has been done? We've added two lines to handle <strong>carddav</strong> and <strong>caldav</strong>, and we removed the paths from <strong>webfinger</strong> and <strong>nodeinfo</strong>. What's left is to restart the pods - this is really important, and we need to add the redirects to our Cloudflare instance.</p>
<p>They say a image says more than a thousand words.. so here goes</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712830499077/1b6e2628-e468-4825-a8c2-28665eb65efb.png" alt class="image--center mx-auto" /></p>
<p>I haven't included <strong>webfinger</strong> and <strong>nodeinfo</strong> to the redirects, if you feel you need them just add them to the config, and remember to deploy your changes.</p>
<p>That's about it. 'till next time!</p>
]]></content:encoded></item><item><title><![CDATA[Installing (Linux) Fedora on very, very old machines]]></title><description><![CDATA[..Or in this case, Fedora 39 on a Macbook Air 2012, with an i7-3667U and 8 GB of ram. And! Just because Apple has discarded it, it doesn't mean this machine is obsolete.. well it kinda is, but it has a lot of life left in it with linux!

💡
This is a...]]></description><link>https://roderik.no/installing-linux-fedora-on-very-very-old-machines</link><guid isPermaLink="true">https://roderik.no/installing-linux-fedora-on-very-very-old-machines</guid><category><![CDATA[Fedora]]></category><category><![CDATA[macbook]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Sat, 10 Feb 2024 09:12:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1707643779156/7e0fdbbe-76a2-4d41-abb4-6b09dd5dc680.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>..Or in this case, Fedora 39 on a Macbook Air 2012, with an i7-3667U and 8 GB of ram. And! Just because Apple has discarded it, it doesn't mean this machine is obsolete.. well it kinda is, but it has a lot of life left in it with linux!</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This is a note-to-self</div>
</div>

<p>, that might come in handy if you in a fit of rage should find your self reading this... fuming over .. things that go boink in the night.</p>
<pre><code class="lang-plaintext">$ sudo dnf groups install --allowerasing -y 'Fedora Workstation'
# or the equivalent for your particular distribution
</code></pre>
<p>The above command is your friend. Embrace it.</p>
<p>Now the desktop loads, yet again, after a lengthy install of media codecs that obviously killed my video driver, and probably some other things as well.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🦾</div>
<div data-node-type="callout-text">Here's a pro tip for ya'll! Enable the <strong>sshd</strong> server until you're done mucking about. Then, disable the <strong>sshd</strong> server - until you need it again!</div>
</div>]]></content:encoded></item><item><title><![CDATA[NextCloud and  Cloudflare Tunnels]]></title><description><![CDATA[Believe me! This his good, wholesome fun!
This write up are instructions on how to successfully set up Nextcloud and cloudflare tunnels resulting, hopefully, in

working Cockpit

working NextCloud

well behaved iOS and Android apps.

persistent, auth...]]></description><link>https://roderik.no/nextcloud-and-cloudflare-tunnels</link><guid isPermaLink="true">https://roderik.no/nextcloud-and-cloudflare-tunnels</guid><category><![CDATA[cloudflare]]></category><category><![CDATA[cloudflared]]></category><category><![CDATA[Nextcloud]]></category><category><![CDATA[cockpit]]></category><category><![CDATA[fedora-server]]></category><category><![CDATA[podman]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Wed, 07 Feb 2024 10:27:05 GMT</pubDate><content:encoded><![CDATA[<p>Believe me! This his good, wholesome fun!</p>
<p>This write up are instructions on how to successfully set up Nextcloud and cloudflare tunnels resulting, hopefully, in</p>
<ul>
<li><p>working Cockpit</p>
</li>
<li><p>working NextCloud</p>
</li>
<li><p>well behaved iOS and Android apps.</p>
</li>
<li><p>persistent, authorized tunnels</p>
</li>
<li><p>a pod that will start automagically after a reboot</p>
</li>
</ul>
<p>First, lets start with Fedora server 39. yeah.. I've got the exact same setup on Fedora server 38 - with the exception that SELinux works perfectly on 38. Not so much on 39. Is it fixable? Yes, yes it is. It's just annoying.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">📣</div>
<div data-node-type="callout-text">Set your SELinux to <strong>permissive</strong> for this installation. You can always turn it back on when and if you have gotten it right.</div>
</div>

<p>Anyways. Why Fedora server? Because of <strong>Cockpit</strong> and <strong>podman</strong>. Not much to it. I happen to like fedora. Get acquainted with Cockpit. It will take some time and will be well worth it. And no matter what anyone may or may not tell you. Cockpit works, it works in production. It is not particularly fragile. It's a good piece of tech. Use it.</p>
<p>This installation guide requires you to get a free Cloudflare account and a domain. You might not need a domain, but this tutorial assumes that you've got one.</p>
<p>Prepare Cloudflare by adding your domain to the Cloudflare dashboard -&gt; Websites. If you haven't got a domain I'm certain Cloudflare can provide you with one. The docs should be able to tell you how to do this.</p>
<h3 id="heading-cloudflared">Cloudflared</h3>
<p>Download <em>cloudflared</em> from this page <a target="_blank" href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/">https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/</a></p>
<p>I'd recommend downloading the binary version and stuffing it in ~/.local/bin with a chmod +x to make cloudflared executable. No one but you should need to have access to it.</p>
<pre><code class="lang-bash">$ cloudflared login <span class="hljs-comment"># follw the instructions.</span>
<span class="hljs-comment"># after successfull authentication you will get a certificate,</span>
<span class="hljs-comment"># and an ID. Take note of both</span>
$ cloudflared tunnel create nextcloud <span class="hljs-comment"># tunnel name can be anything</span>
$ cloudflared tunnel route dns &lt;your-tunnel-id&gt; &lt;subdomain&gt;
<span class="hljs-comment"># You did set up Cloudflare to host your domain.. right.</span>
<span class="hljs-comment"># chose a subdomain.. cloud, for example. That's imaginative.</span>
</code></pre>
<p>Create a new file <em>config.yml</em> - this can be placed anywhere. Your home folder is a good choice (It will eventually get copied to <em>/etc/cloudflared/config.yml)</em></p>
<pre><code class="lang-yaml"><span class="hljs-comment"># contents of config.yml</span>
<span class="hljs-attr">tunnel:</span> <span class="hljs-string">&lt;your-tunnel-id&gt;</span>
<span class="hljs-attr">credentials-file:</span> <span class="hljs-string">/home/&lt;you&gt;/.cloudflared/&lt;your-tunnel-id&gt;.json</span>

<span class="hljs-attr">Ingress:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">hostname:</span> <span class="hljs-string">subdomain.yourdomain.com</span>
      <span class="hljs-attr">service:</span> <span class="hljs-string">http://localhost:8080</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">hostname:</span> <span class="hljs-string">cockpit.yourdomain.com</span>
      <span class="hljs-attr">service:</span> <span class="hljs-string">http://localhost:9090</span>
      <span class="hljs-attr">originRequest:</span>
        <span class="hljs-attr">noTLSVerify:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>Let's detour and <em>fix</em> Cockpit. As <strong>root</strong>, create the file <em>/etc/cockpit/cockpit.conf</em></p>
<pre><code class="lang-ini"><span class="hljs-section">[WebService]</span>
<span class="hljs-attr">Origins</span> = https://cockpit.yourdomain.com wss://cockpit.yourdomain.com
<span class="hljs-attr">ProtocolHeader</span> = X-Forwarded-Proto
<span class="hljs-attr">AllowUnencrypted</span> = <span class="hljs-literal">true</span>
</code></pre>
<p>This will let cockpit be proxied without error through the cloudflared tunnel. Remember to restart cockpit.</p>
<pre><code class="lang-bash">$ sudo systemctl restart cockpit.service
</code></pre>
<p>We're mostly done with the cloudflared stuff.. just one more thing.</p>
<pre><code class="lang-bash">$ sudo /home/&lt;you&gt;/.<span class="hljs-built_in">local</span>/bin/cloudflared \
--config /path/to/config.yml service install <span class="hljs-comment"># as mentioned, this will</span>
<span class="hljs-comment"># copy your config.yml to /etc/cloudflared/config.yml</span>
$ sudo systemctl start cloudflared
$ sudo systemctl status cloudflared
$ sudo systemctl <span class="hljs-built_in">enable</span> cloudflared
</code></pre>
<p>Now our cloudflared tunnel configuration is installed and enabled. It will survive a reboot. Excellent!</p>
<h3 id="heading-on-to-nextcloud">On to NextCloud.</h3>
<p>This is the <strong>Kube</strong> file that will create the pod with all the containers.</p>
<p>Be sure to change out the various passwords and correct the paths, exchanging &lt;you&gt; with your actual account name (I'm trying to make this as beginner friendly as possible, seriously).</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># nextcloud.yaml</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Pod</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">nextcloud</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">nextcloud</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">containers:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">db</span>
      <span class="hljs-attr">args:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">--transaction-isolation=READ-COMMITTED</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">--binlog-format=ROW</span>
      <span class="hljs-attr">command:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">docker-entrypoint.sh</span>
      <span class="hljs-attr">env:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">MYSQL_ROOT_PASSWORD</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">&lt;secret</span> <span class="hljs-string">password&gt;</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">MYSQL_DATABASE</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">nextcloud</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">MYSQL_USER</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">nextcloud</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">MYSQL_PASSWORD</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">&lt;secret</span> <span class="hljs-string">password&gt;</span>
      <span class="hljs-attr">image:</span> <span class="hljs-string">docker.io/library/mariadb:latest</span>
      <span class="hljs-attr">securityContext:</span>
        <span class="hljs-attr">allowPrivilegeEscalation:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">capabilities:</span>
          <span class="hljs-attr">drop:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">CAP_MKNOD</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">CAP_NET_RAW</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">CAP_AUDIT_WRITE</span>
        <span class="hljs-attr">privileged:</span> <span class="hljs-literal">false</span>
        <span class="hljs-attr">readOnlyRootFilesystem:</span> <span class="hljs-literal">false</span>
        <span class="hljs-attr">seLinuxOptions:</span> {}
      <span class="hljs-attr">volumeMounts:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/var/lib/mysql:Z</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">home-&lt;you&gt;-containers-nextcloud-data-db</span>
      <span class="hljs-attr">workingDir:</span> <span class="hljs-string">/</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">app</span>
      <span class="hljs-attr">env:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">REDIS_HOST_PASSWORD</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">&lt;secret</span> <span class="hljs-string">password&gt;</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">MYSQL_HOST</span>
          <span class="hljs-attr">value:</span> <span class="hljs-number">127.0</span><span class="hljs-number">.0</span><span class="hljs-number">.1</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">MYSQL_DATABASE</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">nextcloud</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">REDIS_HOST</span>
          <span class="hljs-attr">value:</span> <span class="hljs-number">127.0</span><span class="hljs-number">.0</span><span class="hljs-number">.1</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">MYSQL_USER</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">nextcloud</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">MYSQL_PASSWORD</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">&lt;the</span> <span class="hljs-string">mysql</span> <span class="hljs-string">password&gt;</span>
      <span class="hljs-attr">image:</span> <span class="hljs-string">docker.io/library/nextcloud:latest</span>
      <span class="hljs-attr">resources:</span> {}
      <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">80</span>
          <span class="hljs-attr">hostPort:</span> <span class="hljs-number">8080</span>
          <span class="hljs-attr">protocol:</span> <span class="hljs-string">TCP</span>
      <span class="hljs-attr">securityContext:</span>
        <span class="hljs-attr">allowPrivilegeEscalation:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">capabilities:</span>
          <span class="hljs-attr">drop:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">CAP_MKNOD</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">CAP_NET_RAW</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">CAP_AUDIT_WRITE</span>
        <span class="hljs-attr">privileged:</span> <span class="hljs-literal">false</span>
        <span class="hljs-attr">readOnlyRootFilesystem:</span> <span class="hljs-literal">false</span>
        <span class="hljs-attr">seLinuxOptions:</span> {}
      <span class="hljs-attr">volumeMounts:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/var/www/html:Z</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">home-&lt;you&gt;-containers-nextcloud-data-html</span>
      <span class="hljs-attr">workingDir:</span> <span class="hljs-string">/var/www/html</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">redis</span>
      <span class="hljs-attr">command:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">redis-server</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">--requirepass</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">&lt;secret</span> <span class="hljs-string">password&gt;</span>
      <span class="hljs-attr">image:</span> <span class="hljs-string">docker.io/library/redis:alpine</span>
      <span class="hljs-attr">resources:</span> {}
      <span class="hljs-attr">securityContext:</span>
        <span class="hljs-attr">allowPrivilegeEscalation:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">capabilities:</span>
          <span class="hljs-attr">drop:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">CAP_MKNOD</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">CAP_NET_RAW</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">CAP_AUDIT_WRITE</span>
        <span class="hljs-attr">privileged:</span> <span class="hljs-literal">false</span>
        <span class="hljs-attr">readOnlyRootFilesystem:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">seLinuxOptions:</span> {}
      <span class="hljs-attr">volumeMounts:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/tmp:Z</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">tmpfs</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/var/tmp:Z</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">tmpfs</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/run:Z</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">tmpfs</span>
      <span class="hljs-attr">workingDir:</span> <span class="hljs-string">/data</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">cron</span>
      <span class="hljs-attr">image:</span> <span class="hljs-string">docker.io/library/nextcloud:latest</span>
      <span class="hljs-attr">command:</span> [<span class="hljs-string">"/cron.sh"</span>]
      <span class="hljs-attr">resources:</span> {}
      <span class="hljs-attr">securityContext:</span>
        <span class="hljs-attr">allowPrivilegeEscalation:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">capabilities:</span>
          <span class="hljs-attr">drop:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">CAP_MKNOD</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">CAP_NET_RAW</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">CAP_AUDIT_WRITE</span>
        <span class="hljs-attr">privileged:</span> <span class="hljs-literal">false</span>
        <span class="hljs-attr">readOnlyRootFilesystem:</span> <span class="hljs-literal">false</span>
        <span class="hljs-attr">seLinuxOptions:</span> {}
      <span class="hljs-attr">volumeMounts:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">mountPath:</span> <span class="hljs-string">/var/www/html:Z</span>
          <span class="hljs-attr">name:</span> <span class="hljs-string">home-&lt;you&gt;-containers-nextcloud-data-html</span>
      <span class="hljs-attr">workingDir:</span> <span class="hljs-string">/var/www/html</span>
  <span class="hljs-attr">restartPolicy:</span> <span class="hljs-string">Always</span>
  <span class="hljs-attr">volumes:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">hostPath:</span>
        <span class="hljs-attr">path:</span> <span class="hljs-string">/home/&lt;you&gt;/containers/nextcloud/data/db</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">Directory</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">home-&lt;you&gt;-containers-nextcloud-data-db</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">hostPath:</span>
        <span class="hljs-attr">path:</span> <span class="hljs-string">/home/&lt;you&gt;/containers/nextcloud/data/html</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">Directory</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">home-&lt;you&gt;-containers-nextcloud-data-html</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">hostPath:</span>
        <span class="hljs-attr">path:</span> <span class="hljs-string">tmpfs</span>
        <span class="hljs-attr">type:</span> <span class="hljs-string">DirectoryOrCreate</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">tmpfs</span>
</code></pre>
<p>Now, create the necessary directories.</p>
<pre><code class="lang-bash">$ <span class="hljs-built_in">cd</span> <span class="hljs-variable">$HOME</span>; \
mkdir -p containers/nextcloud/data/html ;\
mkdir -p containers/nextcloud/data/db
</code></pre>
<p>Great, nearly there. We will be running this as rootless containers, ie. as an unprivileged user (you).. and we will run into trouble for not being allowed to create ports below 1024. So we need to fix that.</p>
<pre><code class="lang-ini"><span class="hljs-comment"># edit /etc/sysctl.conf as root</span>
<span class="hljs-comment"># add this line at the end of the file.</span>
<span class="hljs-attr">net.ipv4.ip_unprivileged_port_start</span>=<span class="hljs-number">80</span>
</code></pre>
<p>You can now either chose to reboot or</p>
<pre><code class="lang-bash">$ sudo sysctl net.ipv4.ip_unprivileged_port_start=80
</code></pre>
<p>Which will temporarily let you use ports below 1024 without rebooting.</p>
<p>It's time to create the pod.</p>
<pre><code class="lang-bash">$ <span class="hljs-comment"># remember, do this a you, the user. Not root!</span>
$ podman kube play path/to/nexcloud.yaml
</code></pre>
<p>This will churn along for quite some time depending on your hardware and internet connection. With fair winds and Odins luck (which you really shouldn't put to much faith in) everything <em>just works</em> and</p>
<pre><code class="lang-bash">$ podman pod ps
POD ID        NAME        STATUS      CREATED            INFRA ID      <span class="hljs-comment"># OF CONTAINERS</span>
96exxxxxxxxx  nextcloud   Running     About an hour ago  6adxxxxxxxxx  5
$ podman ps
6ade3c848dc4  localhost/podman-pause:4.9.0-1706090847                        About an hour ago  Up About an hour  0.0.0.0:8080-&gt;80/tcp  96e88382ac54-infra
59865dbccd75  docker.io/library/mariadb:latest         --transaction-iso...  About an hour ago  Up About an hour  0.0.0.0:8080-&gt;80/tcp  nextcloud-db
f015ddcdb8db  docker.io/library/nextcloud:latest       apache2-foregroun...  About an hour ago  Up About an hour  0.0.0.0:8080-&gt;80/tcp  nextcloud-app
1a22384699bf  docker.io/library/redis:alpine                                 About an hour ago  Up About an hour  0.0.0.0:8080-&gt;80/tcp  nextcloud-redis
12b1bb3f2e0e  docker.io/library/nextcloud:latest                             About an hour ago  Up About an hour  0.0.0.0:8080-&gt;80/tcp  nextcloud-cron
</code></pre>
<p>Yeah.. Now, most stuff has configured it self. But there are .. rough edges.</p>
<p>You'll notice that the IOS and Android app won't be able to connect to your server, so lets fix that.</p>
<p>As root, edit this file <em>/home/&lt;you&gt;/containers/nextcloud/data/html/config/config.php</em></p>
<pre><code class="lang-php"><span class="hljs-comment">/* Add the following structures to your file, 
*  update the scructures  if they allready exists
*/</span>
  <span class="hljs-string">'trusted_proxies'</span> =&gt; 
  <span class="hljs-keyword">array</span> (
    <span class="hljs-number">0</span> =&gt; <span class="hljs-string">'&lt;the ip address of your machine&gt;'</span>,
  ),
  <span class="hljs-string">'overwriteprotocol'</span> =&gt; <span class="hljs-string">'https'</span>,
  <span class="hljs-string">'trusted_domains'</span> =&gt; 
  <span class="hljs-keyword">array</span> (
    <span class="hljs-number">0</span> =&gt; <span class="hljs-string">'localhost'</span>,
    <span class="hljs-number">1</span> =&gt; <span class="hljs-string">'&lt;the ip address of your machine&gt;'</span>,
    <span class="hljs-number">2</span> =&gt; <span class="hljs-string">'subdomain.yourdomain.com'</span>,
  ),

<span class="hljs-comment">/* Also, find and update this variable */</span>
<span class="hljs-string">'overwrite.cli.url'</span> =&gt; <span class="hljs-string">'http://subdomain.yourdomain.com'</span>
</code></pre>
<p>Now, restart your pod.</p>
<p>Did you think we were done? We're not. We need to get the <strong>pod</strong> to start up again in case of catastrophic failure.. like a reboot or what ever. Podman switched to something called Quadlets .. not long ago. It's not difficult, but there's scarce information about how to use it with pods. Well.. not exactly - as long as you realize that pods are Kubes. In either case..</p>
<p>Create a directory</p>
<pre><code class="lang-php">$ cd $HOME; mkdir -p .config/containers/systemd
</code></pre>
<p>In this directory you will create one file named <em>nexcloud.kube</em></p>
<pre><code class="lang-ini"><span class="hljs-comment"># $HOME/.config/containers/systemd/nextcloud.kube</span>
<span class="hljs-section">[Unit]</span>
<span class="hljs-attr">Description</span>=A kubernetes yaml based service
<span class="hljs-attr">Before</span>=local-fs.target
<span class="hljs-section">[Kube]</span>
<span class="hljs-attr">Yaml</span>=/path/to/nextcloud.yaml
<span class="hljs-section">[Install]</span>
<span class="hljs-attr">WantedBy</span>=multi-user.target default.target
</code></pre>
<p>Now run</p>
<pre><code class="lang-ini">$ loginctl enable-linger &lt;you&gt;  <span class="hljs-comment"># for good measure</span>
$ /usr/libexec/podman/quadlets -dryrun -user  <span class="hljs-comment"># to test the .kube</span>
$ systemctl --user daemon-reload  <span class="hljs-comment"># this creates the nextcloud.service</span>
$ systemctl --user start nextcloud.service
</code></pre>
<p>And, we're done.</p>
<p>Now you can enjoy the glory of Cockpit and NextCloud even after a reboot.</p>
<p>'till next time.</p>
]]></content:encoded></item><item><title><![CDATA[It all started with @click]]></title><description><![CDATA[and, well. It went on from there.
I'm going to post some code now.
from collections.abc import Generator
from contextlib import contextmanager, suppress
from dataclasses import dataclass

import click
import requests
from beartype.typing import Any, ...]]></description><link>https://roderik.no/it-all-started-with-click</link><guid isPermaLink="true">https://roderik.no/it-all-started-with-click</guid><category><![CDATA[Click]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Tue, 17 Oct 2023 20:30:53 GMT</pubDate><content:encoded><![CDATA[<p>and, well. It went on from there.</p>
<p>I'm going to post some code now.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> collections.abc <span class="hljs-keyword">import</span> Generator
<span class="hljs-keyword">from</span> contextlib <span class="hljs-keyword">import</span> contextmanager, suppress
<span class="hljs-keyword">from</span> dataclasses <span class="hljs-keyword">import</span> dataclass

<span class="hljs-keyword">import</span> click
<span class="hljs-keyword">import</span> requests
<span class="hljs-keyword">from</span> beartype.typing <span class="hljs-keyword">import</span> Any, Sequence, TypedDict, Unpack

<span class="hljs-keyword">from</span> src.auth <span class="hljs-keyword">import</span> fetch_auth_token
<span class="hljs-keyword">from</span> src.settings <span class="hljs-keyword">import</span> SETTINGS


<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CliOptions</span>(<span class="hljs-params">TypedDict</span>):</span>
    auth_type: str
    chat_type: str
    topic: str | <span class="hljs-literal">None</span>
    participant: str | <span class="hljs-literal">None</span>


<span class="hljs-meta">@dataclass(eq=False)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Options</span>:</span>
    auth_type: str
    chat_type: str
    topic: str | <span class="hljs-literal">None</span>
    participant: str | <span class="hljs-literal">None</span>


<span class="hljs-meta">@contextmanager</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">find_in_instance</span>(<span class="hljs-params">
    cli_options, data
</span>) -&gt; Generator[list[dict[str, <span class="hljs-keyword">None</span> | str | dict[str, str]]], Any, <span class="hljs-keyword">None</span>]:</span>
    match cli_options:
        case Options(participant=<span class="hljs-literal">None</span>, topic=<span class="hljs-literal">None</span>, chat_type=(<span class="hljs-string">"oneOnOne"</span> | <span class="hljs-string">"group"</span> | <span class="hljs-string">"meeting"</span>)):
            <span class="hljs-keyword">yield</span> [item <span class="hljs-keyword">for</span> item <span class="hljs-keyword">in</span> data <span class="hljs-keyword">if</span> cli_options.chat_type == item[<span class="hljs-string">"chatType"</span>]]
        case Options(participant=<span class="hljs-literal">None</span>, chat_type=(<span class="hljs-string">"oneOnOne"</span> | <span class="hljs-string">"group"</span> | <span class="hljs-string">"meeting"</span>)):
            <span class="hljs-keyword">yield</span> [
                item
                <span class="hljs-keyword">for</span> item <span class="hljs-keyword">in</span> data
                <span class="hljs-keyword">if</span> cli_options.chat_type == item[<span class="hljs-string">"chatType"</span>] <span class="hljs-keyword">and</span> cli_options.topic <span class="hljs-keyword">in</span> item[<span class="hljs-string">"topic"</span>.lower()]
            ]
        case Options(topic=<span class="hljs-literal">None</span>, chat_type=(<span class="hljs-string">"oneOnOne"</span> | <span class="hljs-string">"group"</span> | <span class="hljs-string">"meeting"</span>)):
            <span class="hljs-keyword">yield</span> [
                item
                <span class="hljs-keyword">for</span> item <span class="hljs-keyword">in</span> data
                <span class="hljs-keyword">if</span> cli_options.chat_type == item[<span class="hljs-string">"chatType"</span>]
                <span class="hljs-keyword">for</span> member <span class="hljs-keyword">in</span> item[<span class="hljs-string">"members"</span>]
                <span class="hljs-keyword">if</span> cli_options.participant <span class="hljs-keyword">in</span> member[<span class="hljs-string">"displayName"</span>].lower()
            ]
        case Options(chat_type=(<span class="hljs-string">"oneOnOne"</span> | <span class="hljs-string">"group"</span> | <span class="hljs-string">"meeting"</span>)):
            <span class="hljs-keyword">yield</span> [
                item
                <span class="hljs-keyword">for</span> item <span class="hljs-keyword">in</span> data
                <span class="hljs-keyword">if</span> cli_options.chat_type == item[<span class="hljs-string">"chatType"</span>] <span class="hljs-keyword">and</span> cli_options.topic <span class="hljs-keyword">in</span> item[<span class="hljs-string">"topic"</span>]
                <span class="hljs-keyword">for</span> member <span class="hljs-keyword">in</span> item[<span class="hljs-string">"members"</span>]
                <span class="hljs-keyword">if</span> cli_options.participant <span class="hljs-keyword">in</span> member[<span class="hljs-string">"displayName"</span>]
            ]
        case __:
            <span class="hljs-keyword">yield</span> [{<span class="hljs-string">"default"</span>: <span class="hljs-literal">None</span>}]


<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">lowercase</span>(<span class="hljs-params">ctx, param, value</span>):</span>
    <span class="hljs-keyword">if</span> value <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
        <span class="hljs-keyword">return</span> value.lower()


<span class="hljs-meta">@click.command("cli")</span>
<span class="hljs-meta">@click.option(</span>
    <span class="hljs-string">"--auth-type"</span>,
    type=click.Choice(
        [<span class="hljs-string">"interactive"</span>, <span class="hljs-string">"code"</span>],
        case_sensitive=<span class="hljs-literal">False</span>,
    ),
    default=<span class="hljs-string">"interactive"</span>,
    help=<span class="hljs-string">"Default is 'interactive'. If running headless use 'code'"</span>,
)
<span class="hljs-meta">@click.option(</span>
    <span class="hljs-string">"--chat-type"</span>,
    type=click.Choice([<span class="hljs-string">"oneOnOne"</span>, <span class="hljs-string">"group"</span>, <span class="hljs-string">"meeting"</span>], case_sensitive=<span class="hljs-literal">False</span>),
    default=<span class="hljs-string">"oneOnOne"</span>,
    help=<span class="hljs-string">"Chat is either oneOnOne [default],  group or meeting."</span>,
)
<span class="hljs-meta">@click.option(</span>
    <span class="hljs-string">"--topic"</span>,
    callback=lowercase,
    help=<span class="hljs-string">"Filter on chat name or topic when it contains the &lt;string&gt;"</span>,
)
<span class="hljs-meta">@click.option(</span>
    <span class="hljs-string">"--participant"</span>,
    callback=lowercase,
    help=<span class="hljs-string">"Filter on chat participants name when it contains the &lt;string&gt;"</span>,
)
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">cli</span>(<span class="hljs-params">**kwargs: Unpack[CliOptions]</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">recursive_fetch</span>(<span class="hljs-params">
        url: str,
        headers: dict[str, str],
        cli_options: Options,
        index: int = <span class="hljs-number">0</span>,
    </span>) -&gt; int:</span>
        ...do some processing
    ..do auth <span class="hljs-keyword">and</span> some pre prossesing
    cli_options = Options(**kwargs)
    recursive_fetch(url, headers, cli_options)
</code></pre>
<p>So, what are we looking at here? We are looking at a client that fetches chats from https://graph.microsoft.com/v1.0/me/chats?$top=50&amp;$expand=members, recursively.</p>
<p>It started when <code>ruff</code> complained I had too many parameters in my method call. So, naturally, I turned to <code>**kwargs</code> I usually never turn to <code>**kwargs</code> but with TypedDict I feel I can have a clear and concise understanding of what hides inside the <code>dict</code>. So that's one thing. I haven't seen this particular pattern used with click in the wild yet. I might be the only one with this set of needs... or I'm just very poor at designing command-line applications. That might be it.</p>
<p>Anyways. One thing led to another. This is the first time I've used the <code>match</code> statement with any success. I've tried it before but never had a use case that actually resulted in fewer lines and clearer code. Now I think I have.</p>
<p>I will post the auth and the Settings too - when I get around to doing it. For the time being, this post is somewhat of a placeholder so that I don't forget..</p>
]]></content:encoded></item><item><title><![CDATA[contextlib.suppress .. how..?]]></title><description><![CDATA[have I missed this? New in version Python 3.4..
Today I needed to retrieve a long-winded chat history from our corporate Teams .. thingie. Naturally, I fired up m365 and pulled a json from the server.
While parsing the json I got more than a couple o...]]></description><link>https://roderik.no/contextlibsuppress-how</link><guid isPermaLink="true">https://roderik.no/contextlibsuppress-how</guid><category><![CDATA[Python 3]]></category><category><![CDATA[Python]]></category><category><![CDATA[contextlib]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Thu, 05 Oct 2023 11:20:23 GMT</pubDate><content:encoded><![CDATA[<p>have I missed this? New in version Python 3.4..</p>
<p>Today I needed to retrieve a long-winded chat history from our corporate Teams .. thingie. Naturally, I fired up <a target="_blank" href="https://pnp.github.io/cli-microsoft365/">m365</a> and pulled a <code>json</code> from the server.</p>
<p>While parsing the <code>json</code> I got more than a couple of <code>TypeError</code> and <code>KeyError</code>...errors which I squashed with a <code>try-except</code>. All of a sudden I heard <a target="_blank" href="https://github.com/rhettinger">Raymond Hettinger</a> whispering in my ear .. <strong>"there has to be a better way!" -</strong> and lo and behold. Raymond was right!!</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> contextlib <span class="hljs-keyword">import</span> suppress
<span class="hljs-keyword">import</span> json

<span class="hljs-keyword">with</span> open(<span class="hljs-string">"exportedchat.json"</span>, <span class="hljs-string">"r"</span>) <span class="hljs-keyword">as</span> innfile, suppress(TypeError), suppress(KeyError):
    doc = json.loads(fp)
    <span class="hljs-keyword">for</span> chatline <span class="hljs-keyword">in</span> doc:
        print(chatline[<span class="hljs-string">"from"</span>][<span class="hljs-string">"user"</span>][<span class="hljs-string">"displayName"</span>])
</code></pre>
<p>This is so much cleaner than <code>try-&gt;except-&gt;pass</code></p>
]]></content:encoded></item><item><title><![CDATA[podman is not docker]]></title><description><![CDATA[podman is not docker .. yeah. That's an understatement.
This example is a one-shot container. It's not a pod (that might be an upcoming post). I'm running one command in the container and then I remove it. The task is to do some async calls (usually ...]]></description><link>https://roderik.no/podman-is-not-docker</link><guid isPermaLink="true">https://roderik.no/podman-is-not-docker</guid><category><![CDATA[podman]]></category><category><![CDATA[Docker]]></category><category><![CDATA[podman-compose]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Thu, 28 Sep 2023 11:05:11 GMT</pubDate><content:encoded><![CDATA[<p><code>podman</code> is not <code>docker</code> .. yeah. That's an understatement.</p>
<p>This example is a one-shot container. It's not a <code>pod</code> (that might be an upcoming post). I'm running one command in the container and then I remove it. The task is to do some async calls (usually less than 10,000) to Business Central and log the results. The log must be persistent.</p>
<p>On with the show.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># Dockerfile</span>
<span class="hljs-string">FROM</span> <span class="hljs-string">python:3.11-buster</span>
<span class="hljs-string">WORKDIR</span> <span class="hljs-string">/usr/src/myapp</span>

<span class="hljs-string">ADD</span> <span class="hljs-string">src</span> <span class="hljs-string">./src</span>
<span class="hljs-string">COPY</span> <span class="hljs-string">.env</span> <span class="hljs-string">.</span>
<span class="hljs-string">COPY</span> <span class="hljs-string">poetry.lock</span> <span class="hljs-string">.</span>
<span class="hljs-string">COPY</span> <span class="hljs-string">pyproject.toml</span> <span class="hljs-string">.</span>

<span class="hljs-string">RUN</span> <span class="hljs-string">curl</span> <span class="hljs-string">-sSL</span> <span class="hljs-string">https://install.python-poetry.org</span> <span class="hljs-string">|</span> <span class="hljs-string">python3</span> <span class="hljs-bullet">-</span>
<span class="hljs-string">ENV</span> <span class="hljs-string">PATH="/root/.local/bin:$PATH"</span>
<span class="hljs-string">RUN</span> <span class="hljs-string">poetry</span> <span class="hljs-string">config</span> <span class="hljs-string">installer.max-workers</span> <span class="hljs-number">10</span>
<span class="hljs-string">RUN</span> <span class="hljs-string">poetry</span> <span class="hljs-string">install</span> <span class="hljs-string">--only</span> <span class="hljs-string">main</span>
</code></pre>
<p>One image, based on <code>python3.11-buster</code> and a <code>src</code> folder containing the application.</p>
<p>My application must have persistent logging. Coming from <code>docker</code> one would assume that a <code>VOLUMES</code> directive in the <code>Dockerfile</code> or, my preferred method:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># docker-compose.yaml</span>
<span class="hljs-attr">version:</span> <span class="hljs-string">"3"</span>
<span class="hljs-attr">services:</span>
  <span class="hljs-attr">amazingapp:</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">amazingapp</span>
    <span class="hljs-attr">env_file:</span> <span class="hljs-string">.env</span>
    <span class="hljs-attr">build:</span> <span class="hljs-string">.</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./logs:/usr/src/myapp/logs</span>
</code></pre>
<p><code>docker-compose</code> would do the trick. Well. I believe that I've read, somewhere, that <code>docker-compose</code> should <em>just work</em> with <code>podman</code>, but even this minuscule example fails. The <code>VOLUME</code> directive does nothing.</p>
<p>So, enter <code>podman-compose.</code></p>
<p>Does it work? Yes, yes it does. Very well actually.</p>
<pre><code class="lang-bash">$ podman-compose -f docker-compose.yaml build
$ podman-compose -f docker-compose.yaml run --rm amazingapp \
  /path/to/my/script
</code></pre>
<p>To my surprise, this did not create persistent volumes visible for <code>podman volume ls</code>. But it works just as it should.</p>
<p>You might find it easier to run this just using plain <code>podman</code>. After all, it's one less dependency and is perhaps more future-proof.</p>
<pre><code class="lang-bash">$ podman build --tag amazingapp:latest .
$ podman run -it --name myhoovercraftisfulofeels \
  ./logs:/usr/src/myapp/logs:rw,z \
  localhost/amazingapp:latest \
  /path/to/my/script
</code></pre>
<p>Again, it works and no volumes are created.</p>
<p>If you instead execute <code>/bin/bash</code> (replacing <code>/path/to/my/script</code>) you can inspect your running container and find that - indeed the <code>logs</code> directory are being mounted inside the container at the <code>WORKDIR</code> path.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You have no freaking idea how long it took me to realize that <code>WORKDIR</code> was the crux of this matter!</div>
</div>

<pre><code class="lang-bash">$ podman inspect -f <span class="hljs-string">'{{.Mounts}}'</span> &lt;containername&gt;
[{<span class="hljs-built_in">bind</span>  /home/USER/path/to/logs /usr/src/myapp/logs  \
  [rbind] <span class="hljs-literal">true</span> rprivate}
</code></pre>
<p>So what are then <code>volumes?</code></p>
<p>In a pod where you typically would use a <code>kubernetes</code> compatible <code>yaml</code> description you would add a <code>persistentVolumeClaim</code> for each <code>volume</code>. This does basically the same as</p>
<pre><code class="lang-bash">$ podman volume create amazingapp-volume
</code></pre>
<p>Inspecting the <code>volume</code></p>
<pre><code class="lang-bash">$ podman volume inspect amazingapp-volume
[
     {
          <span class="hljs-string">"Name"</span>: <span class="hljs-string">"amazingapp-volume"</span>,
          <span class="hljs-string">"Driver"</span>: <span class="hljs-string">"local"</span>,
          <span class="hljs-string">"Mountpoint"</span>: <span class="hljs-string">"/home/USER/.local/share/containers/storage/ \
           volumes/amazingapp-volume/_data"</span>,
          <span class="hljs-string">"CreatedAt"</span>: <span class="hljs-string">"2023-09-26T08:26:04.236862704+02:00"</span>,
          <span class="hljs-string">"Labels"</span>: {},
          <span class="hljs-string">"Scope"</span>: <span class="hljs-string">"local"</span>,
          <span class="hljs-string">"Options"</span>: {},
          <span class="hljs-string">"MountCount"</span>: 0,
          <span class="hljs-string">"NeedsCopyUp"</span>: <span class="hljs-literal">true</span>,
          <span class="hljs-string">"LockNumber"</span>: 3
     }
]
</code></pre>
<p>Using the <code>volume</code> instead of <code>/home/USER/path/to/logs</code></p>
<pre><code class="lang-bash">$ podman run -it --name myhoovercraftisfulofeels \
  -v amazingapp-volume:/usr/src/myapp/logs:rw,z \
  localhost/amazingapp:latest \
  /path/to/my/script
</code></pre>
<p>Where's your log? It's at:<br /><code>/home/USER/.local/share/containers/storage/volumes/amazingapp-volume/_data</code><br />not particularly convenient.</p>
<p>There are other mounting options as well:</p>
<pre><code class="lang-bash">$ podman run -it --name myhoovercraftisfulofeels \
--mount <span class="hljs-built_in">type</span>=<span class="hljs-built_in">bind</span>,<span class="hljs-built_in">source</span>=/home/USER/path/to/logs,\ 
target=/usr/src/amazingapp localhost/amazingapp:latest \
/path/to/my/script
</code></pre>
<p>Using mount this way, you'll probably run into <code>SELinux</code> problems. Adding <code>--privileged</code> might help, but I'd just stick to the <code>-v</code> and <code>rw,z</code> option to avoid turning off <code>SELinux</code> for the entire container process.</p>
<p>So here we are. This has been a long arduous journey. <code>podman</code> and <code>docker</code> are just different enough to really really trip you up. The takeaways are:</p>
<ul>
<li><p>podman is better</p>
</li>
<li><p>docker has got better documentation</p>
</li>
<li><p>and a heck of a lot larger installed base</p>
</li>
<li><p>but you'll find the answer to your podman question</p>
</li>
<li><p>eventually</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[fprintd for Lenovo thinkpad]]></title><description><![CDATA[So, new (old) laptop.
I recently got my hands on a Lenovo Thinkpad X1 carbon extreme gen4. A nice if somewhat noisy 16" laptop with abysmal battery life.
But, it's got a fingerprint reader.
$ sudo dnf install fprintd libfprint fprintd-pam
$ authselec...]]></description><link>https://roderik.no/fprintd-for-lenovo-thinkpad</link><guid isPermaLink="true">https://roderik.no/fprintd-for-lenovo-thinkpad</guid><category><![CDATA[Linux]]></category><category><![CDATA[#fingerprint ]]></category><category><![CDATA[lenovo]]></category><category><![CDATA[fprintd]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Tue, 12 Sep 2023 10:56:47 GMT</pubDate><content:encoded><![CDATA[<p>So, new (old) laptop.</p>
<p>I recently got my hands on a Lenovo Thinkpad X1 carbon extreme gen4. A nice if somewhat noisy 16" laptop with abysmal battery life.</p>
<p>But, it's got a fingerprint reader.</p>
<pre><code class="lang-bash">$ sudo dnf install fprintd libfprint fprintd-pam
$ authselect enable-feature with-fingerprint
$ authselect apply-changes
</code></pre>
<p>That should do it.. if you've got a gen4.</p>
<p>Upon reboot the fingerprint option will appear in your gnome settings panel.</p>
<p>Not running gnome?</p>
<pre><code class="lang-bash">$ fprintd-enroll <span class="hljs-variable">$USER</span>
</code></pre>
<p>Anyways. The SUDO command also asks for a fingerprint, which may or may not be annoying. Since my machine is docked and attached to two screens while at work, having to use fingerprint for SUDO is a no-go.</p>
<pre><code class="lang-bash">(as root)
$ cp /etc/pam.d/system-auth /etc/pam.d/system-auth-no-fingerprint
</code></pre>
<p>Remove the line containing <strong>pam_fprintd.so</strong> from system-auth-no-fingerprint.</p>
<p>Make a backup of <strong>/etc/pam.d/sudo</strong> and edit the file so that it looks like this.</p>
<pre><code class="lang-bash"><span class="hljs-comment">#%PAM-1.0</span>
auth       include      system-auth-no-fingerprint
account    include      system-auth-no-fingerprint
password   include      system-auth-no-fingerprint
session    optional     pam_keyinit.so revoke
session    required     pam_limits.so
session    include      system-auth-no-fingerprint
</code></pre>
<p>That should do the trick.</p>
<p>Now, how the heck do I unlock my login chain with fingerprint..</p>
]]></content:encoded></item><item><title><![CDATA[Helix editor with multiple lsp's for one language]]></title><description><![CDATA[The helix editor is in its early days. It's quite insanely cool as it stands, and even cooler if you choose which branch to run yourself.

Warning! This is a typical blog post of mine... "lest I forget, do these steps and you'll be fine!"

I did this...]]></description><link>https://roderik.no/helix-editor-with-multiple-lsps-for-one-language</link><guid isPermaLink="true">https://roderik.no/helix-editor-with-multiple-lsps-for-one-language</guid><category><![CDATA[Python]]></category><category><![CDATA[pyright]]></category><category><![CDATA[ruff]]></category><category><![CDATA[lsp]]></category><category><![CDATA[helix editor]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Thu, 09 Mar 2023 11:44:31 GMT</pubDate><content:encoded><![CDATA[<p>The helix editor is in its early days. It's <s>quite</s> insanely cool as it stands, and even cooler if you choose which branch to run yourself.</p>
<blockquote>
<p>Warning! This is a typical blog post of mine... <strong>"lest I forget, do these steps and you'll be fine!"</strong></p>
</blockquote>
<p>I did this on <a target="_blank" href="https://docs.fedoraproject.org/en-US/fedora-silverblue/toolbox/">fedora37 in a toolbox</a>. You don't have to.</p>
<p>The goal is to get <strong>pyright</strong> <em>and</em> <strong>ruff</strong> running for python in Helix.</p>
<h3 id="heading-srsly">Srsly!</h3>
<p>Start by reading the <a target="_blank" href="https://docs.helix-editor.com/install.html">Helix Editor install</a> page. Seriously.</p>
<p>Move on to install things you might or might not be missing. Chances are if you start from a toolbox you'll be missing a lot.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install nvm, node, pyright, tree-sitter, rust, cargo, dev tools..</span>
me@toolbox ~ dnf groupinstall <span class="hljs-string">"Development Tools"</span>
me@toolbox ~ dnf install gcc-c++
me@toolbox ~ dnf install rust cargo
me@toolbox ~ curl https://raw.githubusercontent.com/creationix/nvm/master/install.sh | bash
me@toolbox ~ nvm install v16.19.1
me@toolbox ~ node install pyright tree-sitter tree-sitter-cli
</code></pre>
<p>All this happens in a toolbox. Read about it <a target="_blank" href="https://docs.fedoraproject.org/en-US/fedora-silverblue/toolbox/">here</a>.</p>
<h3 id="heading-there-is-one-particular-helix-branch-that-im-interested-in">There is one particular Helix branch that I'm interested in.</h3>
<blockquote>
<p>Hopefully, these instructions will be obsolete very, very soon.</p>
<p>Read all about it <a target="_blank" href="https://github.com/helix-editor/helix/pull/2507">here</a></p>
</blockquote>
<pre><code class="lang-bash">me@toolbox ~ git <span class="hljs-built_in">clone</span> git@github.com:Philipp-M/helix.git
me@toolbox ~ <span class="hljs-built_in">cd</span> helix
me@toolbox ~ git checkout remotes/origin/multiple-language-servers-with-options-manager
me@toolbox ~ cargo install --locked --path helix-term
me@toolbox ~ ln -s <span class="hljs-variable">$PWD</span>/runtime ~/.config/helix/runtime
</code></pre>
<p>The above is explained on the <a target="_blank" href="https://docs.helix-editor.com/install.html">Helix Editor install</a> page.</p>
<p>The final step is to create <em>~/.config/helix/config.toml</em> and <em>~/.config/helix/languages.toml</em></p>
<pre><code class="lang-yaml"><span class="hljs-comment"># ~/.config/helix/config.toml</span>
<span class="hljs-comment"># This is an example which happens to be how I have helix right now</span>
<span class="hljs-string">theme</span> <span class="hljs-string">=</span> <span class="hljs-string">"base16_transparent"</span>

[<span class="hljs-string">editor</span>]
<span class="hljs-string">line-number</span> <span class="hljs-string">=</span> <span class="hljs-string">"relative"</span>
<span class="hljs-string">scroll-lines</span> <span class="hljs-string">=</span> <span class="hljs-number">1</span>
<span class="hljs-string">scrolloff</span> <span class="hljs-string">=</span> <span class="hljs-number">8</span>
<span class="hljs-literal">true</span><span class="hljs-string">-color</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>

[<span class="hljs-string">editor.cursor-shape</span>]
<span class="hljs-string">insert</span> <span class="hljs-string">=</span> <span class="hljs-string">"bar"</span>

[<span class="hljs-string">editor.lsp</span>]
<span class="hljs-string">display-messages</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>

[<span class="hljs-string">editor.statusline</span>]
<span class="hljs-string">left</span> <span class="hljs-string">=</span> [<span class="hljs-string">"mode"</span>, <span class="hljs-string">"spinner"</span>, <span class="hljs-string">"file-name"</span>]
<span class="hljs-string">center</span> <span class="hljs-string">=</span> []
<span class="hljs-string">mode.normal</span> <span class="hljs-string">=</span> <span class="hljs-string">"NORMAL"</span>
<span class="hljs-string">mode.insert</span> <span class="hljs-string">=</span> <span class="hljs-string">"INSERT"</span>
<span class="hljs-string">mode.select</span> <span class="hljs-string">=</span> <span class="hljs-string">"SELECT"</span>
<span class="hljs-string">right</span> <span class="hljs-string">=</span> [<span class="hljs-string">"diagnostics"</span>, <span class="hljs-string">"selections"</span>, <span class="hljs-string">"position"</span>, <span class="hljs-string">"file-encoding"</span>, <span class="hljs-string">"file-line-ending"</span>, <span class="hljs-string">"file-type"</span>]
<span class="hljs-string">separator</span> <span class="hljs-string">=</span> <span class="hljs-string">"│"</span>

[<span class="hljs-string">editor.indent-guides</span>]
<span class="hljs-string">render</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
<span class="hljs-string">character</span> <span class="hljs-string">=</span> <span class="hljs-string">"|"</span>
</code></pre>
<p>And the language file.</p>
<pre><code class="lang-yaml">[<span class="hljs-string">language-server.pyright</span>]
<span class="hljs-string">command</span> <span class="hljs-string">=</span> <span class="hljs-string">"pyright-langserver"</span>
<span class="hljs-string">args</span> <span class="hljs-string">=</span> [<span class="hljs-string">"--stdio"</span>]

[<span class="hljs-string">language-server.pyright.config</span>]
<span class="hljs-string">lint</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
<span class="hljs-string">inlayHint.enable</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>

[<span class="hljs-string">language-server.ruff-lsp</span>]
<span class="hljs-string">command</span> <span class="hljs-string">=</span> <span class="hljs-string">"ruff-lsp"</span>
[[<span class="hljs-string">language</span>]]
<span class="hljs-string">name</span> <span class="hljs-string">=</span> <span class="hljs-string">"python"</span>
<span class="hljs-string">scope</span> <span class="hljs-string">="source.python"</span>
<span class="hljs-string">auto-format</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
<span class="hljs-string">indent</span> <span class="hljs-string">=</span> { <span class="hljs-string">tab-width</span> <span class="hljs-string">=</span> <span class="hljs-number">4</span>, <span class="hljs-string">unit</span> <span class="hljs-string">=</span> <span class="hljs-string">" "</span> }
<span class="hljs-string">formatter</span> <span class="hljs-string">=</span> { <span class="hljs-string">command</span> <span class="hljs-string">=</span> <span class="hljs-string">"black"</span>, <span class="hljs-string">args</span> <span class="hljs-string">=</span> [<span class="hljs-string">"-"</span>, <span class="hljs-string">"-q"</span>] }
<span class="hljs-string">roots</span> <span class="hljs-string">=</span> [<span class="hljs-string">"pyproject.toml"</span>, <span class="hljs-string">"pyrightconfig.json"</span>, <span class="hljs-string">"Poetry.lock"</span>]
<span class="hljs-string">language-servers</span> <span class="hljs-string">=</span> [
    { <span class="hljs-string">name</span> <span class="hljs-string">=</span> <span class="hljs-string">"pyright"</span> },
    { <span class="hljs-string">name</span> <span class="hljs-string">=</span> <span class="hljs-string">"ruff-lsp"</span> },
]
</code></pre>
<p>Is the language.toml config 100% correct? I don't think so. It works - so there's that, but this is hardly an authoritative source on how to configure <strong>pyright</strong> and <strong>ruff</strong>.</p>
<p>I've found it easiest to just add <strong>ruff-lsp</strong> to my <em>pyproject.toml</em> file and use Helix when in the virtual env.</p>
<p>To make the most of ruff, add this section in your <em>pyproject.toml</em> file</p>
<pre><code class="lang-yaml">[<span class="hljs-string">tool.ruff</span>]
<span class="hljs-string">extend-select</span> <span class="hljs-string">=</span> [<span class="hljs-string">"A"</span>, <span class="hljs-string">"B"</span>, <span class="hljs-string">"C4"</span>, <span class="hljs-string">"D"</span>, <span class="hljs-string">"I"</span>, <span class="hljs-string">"ERA"</span>, <span class="hljs-string">"PLR"</span>, <span class="hljs-string">"SIM"</span>, <span class="hljs-string">"TCH"</span>, <span class="hljs-string">"ANN"</span>]
<span class="hljs-string">show-fixes</span> <span class="hljs-string">=</span> <span class="hljs-literal">true</span>
<span class="hljs-string">line-length=119</span>
</code></pre>
<p>Learn about what it all means <a target="_blank" href="https://beta.ruff.rs/docs/">here</a></p>
<h3 id="heading-at-last">...at last</h3>
<p>..we have arrived at the objective of this blog post. Running two <strong>lsp</strong> servers in helix. This feature is currently unavailable in release 22.12, hence the usage of the <strong>multiple-language-servers-with-options-manager</strong> branch.</p>
<p>There's one more thing I'd like to have as well, namely <strong>inlayHints</strong> <a target="_blank" href="https://github.com/helix-editor/helix/pull/5934">https://github.com/helix-editor/helix/pull/5934</a></p>
<p>I have absolutely no idea how, <a target="_blank" href="https://gist.github.com/msrose/2feacb303035d11d2d05">or if it's even possible, to merge two repos..(turns out it is).</a> I'm hoping for these patches to make it into the next helix release. Until then - I'm happy with having two lsp's.</p>
]]></content:encoded></item><item><title><![CDATA[A Docker for your python scripts]]></title><description><![CDATA[Yes, I know. I did say - somewhere on this blog - that I was podman all the way, baby.
I lied.
Anyways. I needed to deploy a script onto an older server where manually installing python > 3.11 just was too much hassle. I did have docker tho.
First, t...]]></description><link>https://roderik.no/a-docker-for-your-python-scripts</link><guid isPermaLink="true">https://roderik.no/a-docker-for-your-python-scripts</guid><category><![CDATA[Python 3]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Docker compose]]></category><category><![CDATA[python-poetry]]></category><category><![CDATA[Poetry]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Tue, 14 Feb 2023 08:43:22 GMT</pubDate><content:encoded><![CDATA[<p>Yes, I know. I did say - somewhere on this blog - that I was <em>podman all the way, baby.</em></p>
<p>I lied.</p>
<p>Anyways. I needed to deploy a script onto an older server where manually installing python &gt; 3.11 just was too much hassle. I did have docker tho.</p>
<p>First, the <strong>Dockerfile</strong></p>
<pre><code class="lang-yaml"><span class="hljs-string">FROM</span> <span class="hljs-string">python:3.11</span>
<span class="hljs-string">RUN</span> <span class="hljs-string">apt-get</span> <span class="hljs-string">update</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">apt-get</span> <span class="hljs-string">install</span> <span class="hljs-string">-y</span> <span class="hljs-string">\</span>
<span class="hljs-string">curl</span>

<span class="hljs-string">WORKDIR</span> <span class="hljs-string">/usr/src/myapp</span>
<span class="hljs-string">ADD</span> <span class="hljs-string">src</span> <span class="hljs-string">./src</span>
<span class="hljs-string">ADD</span> <span class="hljs-string">logs</span> <span class="hljs-string">./logs</span>

<span class="hljs-string">COPY</span> <span class="hljs-string">.env</span> <span class="hljs-string">.</span>
<span class="hljs-string">COPY</span> <span class="hljs-string">poetry.lock</span> <span class="hljs-string">.</span>
<span class="hljs-string">COPY</span> <span class="hljs-string">pyproject.toml</span> <span class="hljs-string">.</span>

<span class="hljs-string">RUN</span> <span class="hljs-string">curl</span> <span class="hljs-string">-sSL</span> <span class="hljs-string">https://install.python-poetry.org</span> <span class="hljs-string">|</span> <span class="hljs-string">python3</span>
<span class="hljs-string">ENV</span> <span class="hljs-string">PATH="/root/.local/bin:$PATH"</span>
<span class="hljs-string">RUN</span> <span class="hljs-string">poetry</span> <span class="hljs-string">install</span> <span class="hljs-string">--only</span> <span class="hljs-string">main</span>
</code></pre>
<p>Nothing much happening here. We install <em>curl</em> in the image just to be sure. It's probably there already and we do install poetry and add it to the <strong>PATH</strong>.</p>
<p>The <strong>docker-compose.yml</strong></p>
<pre><code class="lang-yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">'3'</span>
<span class="hljs-attr">services:</span>
    <span class="hljs-attr">mymagnificentapp:</span>
        <span class="hljs-attr">container_name:</span> <span class="hljs-string">myapp</span>
        <span class="hljs-attr">env_file:</span> <span class="hljs-string">.env</span>
        <span class="hljs-attr">build:</span> <span class="hljs-string">.</span>
        <span class="hljs-attr">volumes:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">./logs:/usr/src/myapp/logs</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">./src:/usr/src/myapp/src</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">/etc/timezone:/etc/timezone:ro</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">/etc/localtime:/etc/localtime:ro</span>
</code></pre>
<p>We mount both the <em>logs</em> and the <em>src</em> directory. This is afaik the only way to get persistent logs w/o using external logging facilities. We mount the <em>src</em> directory so we can make configuration changes between runs.</p>
<p>The <strong>Makefile</strong></p>
<pre><code class="lang-makefile">CMD := $1

<span class="hljs-section">build:</span>
   docker-compose -f docker-compose.yml build <span class="hljs-variable">$(c)</span>

<span class="hljs-section">firstcmd:</span>
   docker-compose -f docker-compose.yml run mymagnificentapp poetry run src/main1.py

<span class="hljs-section">secondcmd:</span>
   docker-compose -f docker-compose.yml run mymagnificentapp poetry run src/main2.py
</code></pre>
<p>Executing <strong>make</strong> from the command line will build the app. Executing <strong>make firstcmd</strong> will run the defined command.</p>
<blockquote>
<p>However, you might get a permission error if you, like me, have made a habit of forgetting to add the #SHEBANG to your <strong>main.py</strong> files.</p>
</blockquote>
<pre><code class="lang-python"><span class="hljs-comment">#! /usr/bin/env python3</span>
<span class="hljs-string">"""My magnificent main."""</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">main</span>():</span>
    ...

<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    main()
</code></pre>
<p>That's it. You have a semi-isolated python 3.11 environment in which to run scripts.</p>
]]></content:encoded></item><item><title><![CDATA[pyright.createtypestub]]></title><description><![CDATA[As mentioned earlier, this blog is as much a "note to self" as it is a blog..
Today, I needed typesubs for smbclient of package smbprotocol.
:CocCommand pyright.createstypestub smbclient

This creates the necessary typestubs.
In my case I use nvim wi...]]></description><link>https://roderik.no/pyrightcreatetypestub</link><guid isPermaLink="true">https://roderik.no/pyrightcreatetypestub</guid><category><![CDATA[Python 3]]></category><category><![CDATA[nvim, ]]></category><category><![CDATA[smbprotocol]]></category><category><![CDATA[Conqourer of Command]]></category><category><![CDATA[pyright]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Tue, 31 Jan 2023 11:49:14 GMT</pubDate><content:encoded><![CDATA[<p>As mentioned earlier, this blog is as much a "note to self" as it is a blog..</p>
<p>Today, I needed typesubs for <a target="_blank" href="https://pypi.org/project/smbprotocol/">smbclient</a> of package <a target="_blank" href="https://pypi.org/project/smbprotocol/">smbprotocol</a>.</p>
<pre><code class="lang-bash">:CocCommand pyright.createstypestub smbclient
</code></pre>
<p>This creates the necessary typestubs.</p>
<p>In my case I use nvim with <a target="_blank" href="https://github.com/neoclide/coc.nvim">Conquer of Completion</a> and pyright. Yes, I know nvim now has built-in lsp support, but CoC works better for me.</p>
<p>For the stubs to work with pyright you need a <em>pyrightconfig.json</em> file.</p>
<p>It might look something like this</p>
<pre><code class="lang-json">
    <span class="hljs-string">"include"</span>: [
      <span class="hljs-string">"src"</span>,
    ],

    <span class="hljs-string">"exclude"</span>: [
      <span class="hljs-string">"**/__pycache__"</span>
    ],

    <span class="hljs-string">"ignore"</span>: [
    ],
    <span class="hljs-string">"typeshedPath"</span>: <span class="hljs-string">"/home/you/yoursource/typeshed"</span>,
    <span class="hljs-string">"stubPath"</span>: <span class="hljs-string">"/home/you/yoursource/stubs"</span>,
    <span class="hljs-string">"venvPath"</span>: <span class="hljs-string">"."</span>,

    <span class="hljs-string">"reportMissingImports"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">"reportMissingTypeStubs"</span>: <span class="hljs-literal">true</span>,

    <span class="hljs-string">"pythonVersion"</span>: <span class="hljs-string">"3.11"</span>,
    <span class="hljs-string">"pythonPlatform"</span>: <span class="hljs-string">"Linux"</span>,

    <span class="hljs-string">"executionEnvironments"</span>: [
      {
        <span class="hljs-attr">"root"</span>: <span class="hljs-string">"/"</span>,
        <span class="hljs-attr">"venv"</span>: <span class="hljs-string">".venv"</span>,
        <span class="hljs-attr">"pythonVersion"</span>: <span class="hljs-string">"3.11"</span>,
        <span class="hljs-attr">"pythonPlatform"</span>: <span class="hljs-string">"Linux"</span>,
        <span class="hljs-attr">"extraPaths"</span>: [
          <span class="hljs-string">""</span>
        ]
      }
    ]
  }
</code></pre>
<p>and if <em>/src</em> are your sources it should be present alongside your <em>/src</em> directory.</p>
<p>This should solve missing type hints in smbclient. I did however encounter a different problem (this is not always the case).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1675164975869/823660c8-7dd5-45b5-b945-28c677024178.png" alt class="image--center mx-auto" /></p>
<p>To fix this, navigate to your <code>stubs folder /smbclient</code> and edit the <em>__init__.py</em> file in that directory.</p>
<p>In the <em>__init__.py</em> file add <em>__all__</em></p>
<pre><code class="lang-python"><span class="hljs-string">""" 
This type stub file was generated by pyright. 
"""</span> 

<span class="hljs-keyword">import</span> logging 
<span class="hljs-keyword">import</span> smbclient.path
<span class="hljs-keyword">from</span> smbclient._io <span class="hljs-keyword">import</span> SEEK_CUR, SEEK_END, SEEK_SET
<span class="hljs-keyword">from</span> smbclient._os <span class="hljs-keyword">import</span> (
    SMBDirEntry,
    .
    open_file, 
    readlink,
    remove, 
    .
    .
)

<span class="hljs-comment"># add this after all the imports</span>
__all__ = [<span class="hljs-string">"open_file"</span>, <span class="hljs-string">"readlink"</span>, <span class="hljs-string">"remove"</span>, ....]
</code></pre>
<p>and pyright will stop complaining.</p>
<p>There might be a better way of doing this. Please comment if you know of a better way!</p>
]]></content:encoded></item><item><title><![CDATA[I recently migrated to Poetry]]></title><description><![CDATA[I still use pip-tools tho. I find them very convenient. But..
The main reason I'm late to the Poetry party is that I failed to read the docs. I prefer having a .venv in my local dir as opposed to a ~/.config/{virtualenv..cache..thingie} and somehow I...]]></description><link>https://roderik.no/i-recently-migrated-to-poetry</link><guid isPermaLink="true">https://roderik.no/i-recently-migrated-to-poetry</guid><category><![CDATA[Python 3]]></category><category><![CDATA[Poetry]]></category><category><![CDATA[pip-tools]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Tue, 24 Jan 2023 10:45:18 GMT</pubDate><content:encoded><![CDATA[<p>I still use pip-tools tho. I find them very convenient. But..</p>
<p>The main reason I'm late to the Poetry party is that I failed to read the docs. I prefer having a .venv in my local dir as opposed to a ~/.config/{virtualenv..cache..thingie} and somehow I had convinced myself that Poetry didn't support that.</p>
<pre><code class="lang-bash">$ poetry config virtualenvs.in-project <span class="hljs-literal">true</span>
</code></pre>
<p>So with that fixed, I promptly started complaining about Poetrys <code>add &lt;packagename&gt;</code> - time consuming and annoying!! .. but it takes a list of packages.. so if I take my <code>requirements.in</code> and <code>dev-requirements.in</code> files and ..</p>
<pre><code class="lang-bash">$ poetry add $(cat reqs/requirements.in)
$ poetry add $(cat reqs/dev-requirements.in) --group dev
</code></pre>
<p>Yeah, that worked. But hey(!), my deploy scripts expect a <code>requirements.txt?</code> file. I can't be bothered with fixing that!</p>
<pre><code class="lang-bash">$ poetry <span class="hljs-built_in">export</span> --without-hashes --format=requirements.txt &gt; requirements.txt
</code></pre>
<p>Damnation!</p>
<p>As you well understand, by now I've run out of excuses for not using Poetry.</p>
<p>One down, one more to go. Now, onwards towards integrating Poetry with my build setup.</p>
]]></content:encoded></item><item><title><![CDATA[Python class generation from YAML]]></title><description><![CDATA[YAML - I remember using YAML extensively back in the day. It somehow fell out of favour, there was a new kid on the block - JSON. This story is not about JSON.
I'm using the pyYaml library in this write-up.
# companies.yaml
- !Company
  company_name:...]]></description><link>https://roderik.no/python-class-generation-from-yaml</link><guid isPermaLink="true">https://roderik.no/python-class-generation-from-yaml</guid><category><![CDATA[Python]]></category><category><![CDATA[YAML]]></category><category><![CDATA[protocols]]></category><category><![CDATA[beartype]]></category><dc:creator><![CDATA[Rune Hansén Steinnes-Westum]]></dc:creator><pubDate>Thu, 19 Jan 2023 13:24:28 GMT</pubDate><content:encoded><![CDATA[<p>YAML - I remember using YAML extensively back in the day. It somehow fell out of favour, there was a new kid on the block - JSON. This story is not about JSON.</p>
<p>I'm using the pyYaml library in this write-up.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># companies.yaml</span>
<span class="hljs-bullet">-</span> <span class="hljs-type">!Company</span>
  <span class="hljs-attr">company_name:</span> <span class="hljs-string">PoundCompany</span>
  <span class="hljs-attr">active:</span> <span class="hljs-literal">True</span> 
  <span class="hljs-attr">has_dimensions:</span> <span class="hljs-literal">True</span>
  <span class="hljs-attr">dimensions:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">sillyname1</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">sillyname2</span>
  <span class="hljs-attr">file_mapping:</span>
    <span class="hljs-attr">account_number_map:</span> <span class="hljs-string">accounts.csv</span>
    <span class="hljs-attr">account_type_map:</span> <span class="hljs-string">accounts.csv</span>
    <span class="hljs-attr">tax_code_map:</span> <span class="hljs-string">taxcodes.csv</span>
  <span class="hljs-attr">bi_central_uuid:</span> <span class="hljs-number">00000000</span><span class="hljs-number">-0000</span><span class="hljs-number">-0000</span><span class="hljs-number">-0000</span><span class="hljs-number">-000000000001</span>
<span class="hljs-bullet">-</span> <span class="hljs-type">!Company</span>
  <span class="hljs-attr">company_name:</span> <span class="hljs-string">EuroCompany</span>
  <span class="hljs-attr">active:</span> <span class="hljs-literal">True</span> 
  <span class="hljs-attr">has_dimensions:</span> <span class="hljs-literal">True</span>
  <span class="hljs-attr">dimensions:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">sillyname1</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">sillyname2</span>
  <span class="hljs-attr">file_mapping:</span>
    <span class="hljs-attr">account_number_map:</span> <span class="hljs-string">accounts.csv</span>
    <span class="hljs-attr">account_type_map:</span> <span class="hljs-string">accounts.csv</span>
    <span class="hljs-attr">tax_code_map:</span> <span class="hljs-string">taxcodes.csv</span>
  <span class="hljs-attr">bi_central_uuid:</span> <span class="hljs-number">00000000</span><span class="hljs-number">-0000</span><span class="hljs-number">-0000</span><span class="hljs-number">-0000</span><span class="hljs-number">-000000000002</span>
</code></pre>
<p>This YAML describes two instances of the initialized class.</p>
<pre><code class="lang-python"><span class="hljs-comment">#company.py</span>
<span class="hljs-keyword">from</span> dataclasses <span class="hljs-keyword">import</span> dataclass, field

<span class="hljs-meta">@dataclass</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CompanyModel</span>:</span>
    company_name: str = field(default=<span class="hljs-string">""</span>)
    active: bool = <span class="hljs-literal">False</span>
    has_dimensions: bool = <span class="hljs-literal">False</span>
    dimensions: list = field(default_factory=list)
    file_mapping: dict = field(default_factory=dict)
    bi_central_uuid: str = field(default=<span class="hljs-string">""</span>)
</code></pre>
<p>And this is my class. It might surprise you that I've chosen to name it xxxModel. To me, this is a model. A collection of attributes that I, at some point, am going to serialize to JSON before posting it on the Business Central API. This class is basically data.</p>
<p>The task at hand is to go from YAML to (model)Class!</p>
<p>The pyYaml documentation has a few examples on how to do this, none of which I found particularly interesting.</p>
<p>Onwards! Let's start with reading the YAML</p>
<pre><code class="lang-python"><span class="hljs-comment"># company_loader.py</span>
<span class="hljs-keyword">import</span> yaml
<span class="hljs-keyword">from</span> protocols <span class="hljs-keyword">import</span> Settings, Company
<span class="hljs-keyword">from</span> settings <span class="hljs-keyword">import</span> SETTINGS
<span class="hljs-keyword">from</span> company <span class="hljs-keyword">import</span> CompanyModel
<span class="hljs-keyword">from</span> beartype <span class="hljs-keyword">import</span> beartype

<span class="hljs-meta">@beartype</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_companies</span>(<span class="hljs-params">settings: Settings</span>):</span>
    <span class="hljs-keyword">with</span> open(settings.base_dir / <span class="hljs-string">"companies.yaml"</span>, <span class="hljs-string">"rb"</span>) <span class="hljs-keyword">as</span> fp:
        <span class="hljs-keyword">return</span> yaml.load(fp, Loader=get_loader())
</code></pre>
<p>The interesting part here is <strong>get_loader().</strong> Let's define it.</p>
<pre><code class="lang-python"><span class="hljs-comment"># company_loader.py</span>
<span class="hljs-meta">@beartype</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_loader</span>()-&gt;type[yaml.SafeLoader]:</span>
    loader = yaml.SafeLoader
    loader.add_constructor(<span class="hljs-string">"!Company"</span>, company_constructor)
    <span class="hljs-keyword">return</span> loader
</code></pre>
<p>Now we have created the loader and we've added a constructor to it. The constructor does all the heavy lifting and will construct our class. There are a few constructors in pyYaml. To be honest, I gave <strong>yaml.SafeLoader.construct_mapping()</strong> a go and never figured out why it didn't produce lists and dicts. This is far from rocket science and creating a constructor is relatively easy</p>
<blockquote>
<p>This might be a good time to explain why I created this contraption. I needed to move data (appearing in one and only one static format) to the BiCentral API. No room for interpretation.</p>
</blockquote>
<p>Anyway. You'll find code in pyYaml that resembles the below function - the main difference being that I understand what my code does and it does exactly what I need.</p>
<pre><code class="lang-python"><span class="hljs-comment"># company_loader.py</span>
<span class="hljs-meta">@beartype</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">company_constructor</span>(<span class="hljs-params">
    loader: yaml.SafeLoader, 
    node: yaml.nodes.MappingNode,
    </span>)-&gt; Company:</span>                                                                     
    <span class="hljs-string">"""Construct an employee."""</span>                                                                                                                                    
    _node = {}                                                                                                                         

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">process_node</span>(<span class="hljs-params">node</span>):</span> -&gt; <span class="hljs-literal">None</span>                                                                                                                                 
        <span class="hljs-keyword">for</span> _n <span class="hljs-keyword">in</span> node.value:                                                                                                                                       
            attr_name, attr_value = _n                                                                                                                   
            <span class="hljs-keyword">if</span> isinstance(attr_name, yaml.MappingNode):                                                                                                             
                process_node(attr_name)                                                                                                                             
            _node[attr_name.value] = <span class="hljs-string">""</span>                                                                                                                             
            _cast: str = attr_value.tag.split(<span class="hljs-string">":"</span>)[<span class="hljs-number">-1</span>]
            <span class="hljs-keyword">if</span> _cast <span class="hljs-keyword">in</span> [<span class="hljs-string">"bool"</span>, <span class="hljs-string">"int"</span>]:                                                                                                                            
                _node[attr_name.value] = eval(attr_value.value)                                                                                                     
            <span class="hljs-keyword">elif</span> _cast == <span class="hljs-string">"seq"</span>:                                                                                                                                    
                _node[attr_name.value] = [
                    item.value <span class="hljs-keyword">for</span> item <span class="hljs-keyword">in</span>
                    attr_value.value
                    ]                                                                                  
            <span class="hljs-keyword">elif</span> _cast == <span class="hljs-string">"map"</span>:                                                                                                                                    
                _node[attr_name.value] = {}                                                                                                                         
                _node[attr_name.value].update(                                                                                                                      
                    {
                        (key.value, val.value) <span class="hljs-keyword">for</span> (key, val)
                         <span class="hljs-keyword">in</span> [
                                item <span class="hljs-keyword">for</span> item <span class="hljs-keyword">in</span>
                                attr_value.value
                             ]
                    },                                                                 
                )                                                                                                                                                   
            <span class="hljs-keyword">else</span>:                                                                                                                                                   
                _node[attr_name.value] = attr_value.value
    process_node(node)                                                                                                                                              
    <span class="hljs-keyword">return</span> CompanyModel(**_node)  <span class="hljs-comment"># type: ignore</span>
</code></pre>
<p>As you can see, the <strong>process_node()</strong> function is recursive and it is tailored to my needs. I don't need to call eval on items in my list or dict instances so naturally, I don't. When it's done I unpack the resulting dict and return an initialized CompanyModel object.</p>
<p>Before wrapping up I'd like to mention two minor details. I really like protocols (Thank you <a target="_blank" href="https://www.youtube.com/@ArjanCodes">Arjan</a>) and beartype - I also like <a target="_blank" href="https://pypi.org/project/beartype/">beartype</a>, a lot.</p>
<pre><code class="lang-python"><span class="hljs-comment"># protocols.py</span>
<span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> Protocol
<span class="hljs-keyword">from</span> pathlib <span class="hljs-keyword">import</span> Path
<span class="hljs-keyword">from</span> beartype <span class="hljs-keyword">import</span> typing

<span class="hljs-meta">@typing.runtime_checkable                                                                                                                                           </span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Settings</span>(<span class="hljs-params">Protocol</span>):</span>                                                                                                                                           
    base_dir: Path

<span class="hljs-meta">@typing.runtime_checkable</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Company</span>(<span class="hljs-params">Protocol</span>):</span>
    company_name: str
    active: bool
    has_dimensions: bool
    dimensions: list
    file_mapping: dict
    bi_central_uuid: str
</code></pre>
<p>You'll find the SETTINGS class somewhere else on this blog.</p>
<p>The main takeaways.</p>
<ul>
<li><p>YAML is still cool</p>
</li>
<li><p>Things do not need to get too complicated if you scope it well</p>
</li>
<li><p>Protocols are awesome</p>
</li>
<li><p>Arjan is awesome</p>
</li>
<li><p>Beartyping is next-level awesome</p>
</li>
</ul>
]]></content:encoded></item></channel></rss>