<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Posts on Daan Geijs</title>
    <link>https://www.daangeijs.nl/posts/</link>
    <description>Recent content in Posts on Daan Geijs</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Thu, 06 Mar 2025 12:48:00 +0100</lastBuildDate><atom:link href="https://www.daangeijs.nl/posts/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Fixing ASPM and Deep C-States on Intel NUC 7th gen</title>
      <link>https://www.daangeijs.nl/posts/cstates-nuc/</link>
      <pubDate>Thu, 06 Mar 2025 12:48:00 +0100</pubDate>
      
      <guid>https://www.daangeijs.nl/posts/cstates-nuc/</guid>
      <description>If you own an Intel NUC and are struggling to reach deep power-saving states like C8 or C9, your issue might be related to PCIe Active State Power Management (ASPM). I recently ran into this problem with my Intel NUC 7th Gen, where the system was stuck at C3 states despite my best efforts to tweak Linux power settings.</description>
      <content:encoded><![CDATA[<h3 id="introduction">Introduction</h3>
<p>If you own an Intel NUC and are struggling to reach deep power-saving states like C8 or C9, your issue might be related to PCIe Active State Power Management (ASPM). I recently ran into this problem with my Intel NUC 7th Gen, where the system was stuck at C3 states despite my best efforts to tweak Linux power settings. Through trial and error, I discovered that an outdated BIOS was blocking ASPM on my NVMe SSD, preventing the system from reaching deeper C-states. A BIOS update ultimately resolved the issue! If you&rsquo;re facing a similar problem, this guide will walk you through the solution.</p>
<h3 id="the-problem-why-was-my-intel-nuc-stuck-at-c3">The Problem: Why Was My Intel NUC Stuck at C3?</h3>
<p>I started with PowerTOP, an essential diagnostic tool for Linux power management. To install it on most Linux distributions, use one of these commands:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt-get install powertop
</span></span></code></pre></div><p>After installation, you can run PowerTOP with privileges:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo powertop
</span></span></code></pre></div><h3 id="checking-power-states-with-powertop">Checking Power States with PowerTOP</h3>
<p>After running PowerTOP and going to the &lsquo;Idle Stats&rsquo; tab, I noticed that my system was only reaching C3 states, instead of the deeper C8-C10 states that Intel CPUs support. So clearly, something was blocking my system from reaching deeper states. The first thing that I tried was to run auto-tune. PowerTOP includes this feature that applies various power-saving settings:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo powertop --auto-tune
</span></span></code></pre></div><p>This command applies recommended settings for all power management features. However, in my case, even after running auto-tune, the system remained stuck at C3 states, indicating a deeper issue.</p>
<h3 id="investigating-pcie-aspm-with-lspci">Investigating PCIe ASPM with <code>lspci</code></h3>
<p>Since NVMe SSDs and PCIe devices can block deep C-states, I checked their ASPM support, and I easily found one device not being ASPM enabled.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo lspci -vv <span class="p">|</span> awk <span class="s1">&#39;/ASPM/{print $0}&#39;</span> <span class="nv">RS</span><span class="o">=</span> <span class="p">|</span> grep --color -P <span class="s1">&#39;(^[a-z0-9:.]+|ASPM )&#39;</span>
</span></span></code></pre></div><p>I noticed multiple things:</p>
<ul>
<li>My Samsung SM961 NVMe SSD (3a:00.0) supports ASPM L1, but it was disabled.</li>
<li>The PCIe Root Port (00:1d.0) did not support ASPM.</li>
<li>Another PCIe Root Port (00:1c.0) did support ASPM, but nothing was connected to it.</li>
</ul>
<p>This suggested that my NVMe SSD was stuck on a PCIe port that does not support ASPM, blocking deep sleep states.</p>
<h3 id="forcing-aspm-enable">Forcing ASPM Enable</h3>
<p>Before diving into BIOS updates, I tried a helpful script from GitHub called AutoASPM, which attempts to enable ASPM for all devices, hoping this would enable ASPM.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Clone the repository</span>
</span></span><span class="line"><span class="cl">git clone https://github.com/notthebee/AutoASPM
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> AutoASPM
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Run the script (requires sudo)</span>
</span></span><span class="line"><span class="cl">sudo python3 autoaspm.py
</span></span></code></pre></div><p>This script tries to enable ASPM across all PCI devices by writing to the appropriate kernel interface files. However, in my case, since the BIOS had disabled ASPM at a hardware level, the script couldn&rsquo;t overcome this limitation. After running again:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo lspci -vv <span class="p">|</span> awk <span class="s1">&#39;/ASPM/{print $0}&#39;</span> <span class="nv">RS</span><span class="o">=</span> <span class="p">|</span> grep --color -P <span class="s1">&#39;(^[a-z0-9:.]+|ASPM )&#39;</span>
</span></span></code></pre></div><p>I still noticed that my ASPM was disabled for the NVMe.</p>
<p>If you&rsquo;re facing similar issues, try this script first—it may solve your problem without requiring a BIOS update.</p>
<h3 id="checking-bios-configuration-for-aspm">Checking BIOS Configuration for ASPM</h3>
<p>After trying software solutions without success, I decided to check if my BIOS was configured correctly for ASPM. I confirmed that <strong>PCIe ASPM Support</strong> was enabled in the BIOS.</p>
<figure>
    <img loading="lazy" src="image_bios.jpg"/> 
</figure>

<p>Despite this correct configuration in the BIOS, my system still could not reach deeper C-states. This led me to conclude that either the port my NVMe SSD was using did not allow lower power states. However, this conclusion did not make sense to me—why would an Intel NUC engineer design it this way? So the next day, I decided to give it one last try and check if this was an issue that was solved by a BIOS update.</p>
<h3 id="the-fix-updating-my-bios">The Fix: Updating My BIOS</h3>
<p>As a last resort, I checked Intel&rsquo;s website for a newer BIOS version (which is now moved to ASUS) for my NUC model (NUC7i5BNK). I found a BIOS update <a href="https://www.asus.com/supportonly/nuc7i5bnk/helpdesk_bios/">here</a> on the ASUS website.</p>
<p>I followed the instructions:</p>
<ul>
<li>Downloaded the ZIP file.</li>
<li>Unzipped and added the <code>.bio</code> file to a USB flash drive.</li>
<li>Booted my NUC with the USB flash drive while spamming <code>F7</code>.</li>
<li>Selected the <code>.bio</code> file from the menu and let the system update itself.</li>
</ul>
<p>Once the BIOS update was complete, I quickly verified that my BIOS settings were still untouched— PCIe ASPM Support was still enabled. After rebooting, I ran the same power state checks:</p>
<p>Now I was able to run AutoASPM and verify that it was working with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo lspci -vv <span class="p">|</span> awk <span class="s1">&#39;/ASPM/{print $0}&#39;</span> <span class="nv">RS</span><span class="o">=</span> <span class="p">|</span> grep --color -P <span class="s1">&#39;(^[a-z0-9:.]+|ASPM )&#39;</span>
</span></span></code></pre></div><p>And after running:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo powertop
</span></span></code></pre></div><p>I noticed that the output now showed C8 and even C9 states, with 20% less power consumption! Okay, I&rsquo;ll be honest with you— 3 watts saved. But hey, free energy savings!</p>
<h3 id="bonus-enabling-aspm-for-other-devices">Bonus: Enabling ASPM for Other Devices</h3>
<p>Through this process, I became faster in checking the ASPM states of all devices, which really helped me in another project. For example, there I immediately identified one device that was not ASPM-enabled. Back then enabled it manually with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="m">1</span> <span class="p">|</span> sudo tee /sys/bus/pci/drivers/r8169/0000:01:00.0/link/l1_aspm
</span></span></code></pre></div><p>However, by going all the way on this NUC, I now discovered this useful Python script. I think for each system, you could run AutoASPM, along with <code>powertop --auto-tune</code>, and verify with the regex <code>lspci</code> command to get a very high success rate of reaching deep C-states.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>LaTeX in Visual Studio Code (VSCode) on macOS</title>
      <link>https://www.daangeijs.nl/posts/latex-vscode/</link>
      <pubDate>Tue, 10 Sep 2024 12:48:00 +0100</pubDate>
      
      <guid>https://www.daangeijs.nl/posts/latex-vscode/</guid>
      <description>When writing my thesis, I found Overleaf to be a bit too minimalistic for managing a large project such as a PhD Thesis. Installing LaTeX in Visual Studio Code (VSCode) really helped me to work in a modern, customizable text editor. In this post, we&amp;rsquo;ll go through the process of installing LaTeX on macOS using Homebrew, configuring VSCode with LaTeX Workshop, and syncing with Overleaf for collaborative writing and reviewing. Please note that the LaTeX installation can take a while, since updating and downloading packages can be time-consuming.</description>
      <content:encoded><![CDATA[<p>When writing my thesis, I found Overleaf to be a bit too minimalistic for managing a large project such as a PhD Thesis. Installing LaTeX in Visual Studio Code (VSCode) really helped me to work in a modern, customizable text editor. In this post, we&rsquo;ll go through the process of installing LaTeX on macOS using Homebrew, configuring VSCode with LaTeX Workshop, and syncing with Overleaf for collaborative writing and reviewing. Please note that the LaTeX installation can take a while, since updating and downloading packages can be time-consuming.</p>
<h2 id="step-1-install-homebrew-if-not-already-installed">Step 1: Install Homebrew (if not already installed)</h2>
<p>First, if you don&rsquo;t have Homebrew installed, open your terminal and enter the following command:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">/bin/bash -c <span class="s2">&#34;</span><span class="k">$(</span>curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh<span class="k">)</span><span class="s2">&#34;</span>
</span></span></code></pre></div><p>This will install Homebrew, a package manager for macOS, making it easy to install LaTeX and other software.</p>
<h2 id="step-2-install-latex-via-homebrew">Step 2: Install LaTeX via Homebrew</h2>
<p>Once Homebrew is installed, you can install LaTeX by running the following command in your terminal:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">brew install --cask mactex-no-gui
</span></span></code></pre></div><p>This command installs the <strong>MacTeX-no-GUI</strong> version, which includes the full LaTeX environment without any graphical applications (such as TeXShop). The download and installation can take a while to start, so be patient—it may seem like nothing is happening initially, but it will eventually begin downloading.</p>
<p>After installation, update LaTeX packages to ensure you have the latest versions. You will need to restart your terminal before running the following commands:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tlmgr update --self
</span></span><span class="line"><span class="cl">sudo tlmgr update --all
</span></span></code></pre></div><p>This ensures your LaTeX environment is fully up to date. If you encounter any issues during the installation or update process, check the terminal output for error messages and try to resolve them accordingly. For me, it helped to run <code>eval &quot;$(/usr/libexec/path_helper)&quot;</code> and restart my terminal.</p>
<h2 id="step-3-install-vscode">Step 3: Install VSCode</h2>
<p>If you haven&rsquo;t installed Visual Studio Code yet, you can download and install it from the <a href="https://code.visualstudio.com/">official VSCode website</a>.</p>
<p>Once installed, open VSCode and prepare to add the necessary extensions for LaTeX.</p>
<h2 id="step-4-install-the-latex-workshop-extension">Step 4: Install the LaTeX Workshop Extension</h2>
<p>VSCode doesn&rsquo;t natively support LaTeX, so we need to install an extension called <strong>LaTeX Workshop</strong>. This extension provides LaTeX syntax highlighting, compilation, previews, and other useful features.</p>
<p>To install it:</p>
<ol>
<li>Open <strong>VSCode</strong>.</li>
<li>Go to the <strong>Extensions</strong> view by clicking on the Extensions icon on the sidebar or pressing <code>Cmd</code> + <code>Shift</code> + <code>X</code>.</li>
<li>Search for <strong>LaTeX Workshop</strong>.</li>
<li>Click <strong>Install</strong>.</li>
</ol>
<p>Once installed, LaTeX Workshop will automatically manage compiling your LaTeX files and displaying previews.</p>
<h3 id="configuring-latex-workshop">Configuring LaTeX Workshop:</h3>
<p>You can customize LaTeX Workshop by going to the settings (open <code>Cmd</code> + <code>,</code>) and searching for <strong>LaTeX Workshop</strong>. You’ll find options for how you want your documents to compile, preview behavior, and more. For now, you can leave the default settings as they are.</p>
<h2 id="step-5-enable-word-wrap-in-vscode">Step 5: Enable Word Wrap in VSCode</h2>
<p>LaTeX files can often contain long lines of text, and horizontal scrolling is inconvenient. To make your experience smoother, enable <strong>word wrap</strong> in VSCode so that long lines break automatically within the window.</p>
<h3 id="to-enable-word-wrap-globally">To enable word wrap globally:</h3>
<ol>
<li>Open VSCode settings (<code>Cmd</code> + <code>,</code>).</li>
<li>Search for <strong>word wrap</strong>.</li>
<li>Set <strong>Editor: Word Wrap</strong> to <code>on</code>.</li>
</ol>
<p>Alternatively, you can set word wrap for individual sessions by using the keyboard shortcut <code>Alt</code> + <code>Z</code>.</p>
<p>Another way to enable word wrap is by using the <strong>Command Palette</strong>:</p>
<ol>
<li>Open the Command Palette by pressing <code>Cmd</code> + <code>Shift</code> + <code>P</code>.</li>
<li>Type <strong>Word Wrap</strong> and select <strong>View: Toggle Word Wrap</strong>.</li>
</ol>
<p>This ensures that lines automatically break without requiring horizontal scrolling.</p>
<h2 id="step-6-syncing-vscode-with-overleaf-for-collaboration-and-reviews">Step 6: Syncing VSCode with Overleaf for Collaboration and Reviews</h2>
<p>While VSCode offers a great environment for writing and compiling LaTeX documents, Overleaf is fantastic for collaboration, reviewing, and sharing with others. You can leverage both platforms by syncing your project between Overleaf and VSCode.</p>
<h3 id="setting-up-overleaf-for-git-synchronization">Setting up Overleaf for Git Synchronization</h3>
<ol>
<li>
<p><strong>Create or Open a Project on Overleaf</strong>:
Start by either creating a new project on Overleaf or selecting an existing project that you want to sync with.</p>
</li>
<li>
<p><strong>Enable Git on Overleaf</strong>:
Overleaf allows you to connect your project to a private Git repository. To do this:</p>
<ul>
<li>Open your Overleaf project.</li>
<li>Go to the <strong>Menu</strong> (top left).</li>
<li>Scroll down to <strong>GitHub</strong>.</li>
<li>Follow the instructions to allow Overleaf to create a private Git repository for your project.</li>
</ul>
</li>
<li>
<p><strong>Clone the Repository in VSCode</strong>:
Once Overleaf creates the repository, you can clone it to your local machine:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone &lt;your-overleaf-git-url&gt;
</span></span></code></pre></div><p>After cloning, you can open the project in VSCode.</p>
</li>
<li>
<p><strong>Add a .gitignore file</strong>:
Add a <code>.gitignore</code> file to your project to exclude unnecessary LaTeX-generated files (like <code>.aux</code>, <code>.log</code>, etc.). A nice <code>.gitignore</code> template for LaTeX projects can be found <a href="https://github.com/github/gitignore/blob/main/TeX.gitignore">here</a>.</p>
</li>
<li>
<p><strong>Working in VSCode</strong>:
You can now work on your LaTeX document in VSCode, take advantage of the full LaTeX Workshop experience, and perform heavy-duty tasks like editing large files or compiling complex documents.</p>
</li>
<li>
<p><strong>Syncing with Overleaf</strong>:
If you want to review or share a version, simply push your changes back to the Overleaf repository:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git push origin main
</span></span></code></pre></div><p>Overleaf will not automatically update with the latest version, for this you will have to manually pull the changes from GitHub in Overleaf.</p>
<p>To do this:</p>
<ul>
<li>Open your Overleaf project.</li>
<li>Go to the <strong>Menu</strong> (top left).</li>
<li>Scroll down to <strong>GitHub</strong>.</li>
<li>Press the button to <strong>Pull changes from GitHub</strong>.</li>
</ul>
<p>Make sure to resolve any conflicts that may arise during the pull process. The best way is to make sure everything is pushed to GitHub before pulling in Overleaf.</p>
</li>
<li>
<p><strong>Collaborate and Review on Overleaf</strong>:
Once your collaborators or reviewers have made changes on Overleaf, you can pull those changes back into your local repository:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git pull origin main
</span></span></code></pre></div><p>This way, you can synchronize changes between Overleaf and VSCode seamlessly, using Overleaf for easy collaboration and VSCode for more ease of work.</p>
</li>
</ol>
<h2 id="step-7-compile-and-preview">Step 7: Compile and Preview</h2>
<p>Here is a quick preview of how it looks when you have set everything up. You can see the LaTeX code on the left and the preview on the right. By default, auto-compilation is enabled, which means the preview will update automatically when you save the file (or have autosave enabled). I recommend enabling autosave on all your projects. In this case, I switched my theme to a light theme because the dark-themed editor created too much contrast compared to the preview.</p>
<p><img loading="lazy" src="/posts/latex-vscode/image.png" type="" alt="alt text"  /></p>
<p>With LaTeX installed via Homebrew and configured in VSCode using the LaTeX Workshop extension, you now have a powerful setup for writing LaTeX documents. Additionally, by syncing your project with Overleaf, you can easily collaborate with others, review changes, and keep everything in sync using Git. The installation and setup may take some time, especially when using the <code>brew</code> command, but once it&rsquo;s complete, you’ll have a smooth and customizable LaTeX environment.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Remote software development with VSCode and Docker Compose</title>
      <link>https://www.daangeijs.nl/posts/remote-docker-development-vscode/</link>
      <pubDate>Tue, 03 Oct 2023 12:48:00 +0100</pubDate>
      
      <guid>https://www.daangeijs.nl/posts/remote-docker-development-vscode/</guid>
      <description>Modern software development often requires us to juggle multiple services and environments. When you add the constraint of sensitive data or specific hardware requirements, this complexity can grow. Fortunately, tools like VSCode, Docker, and SSH are here to simplify our lives. In this post, we&amp;rsquo;ll walk through setting up a development environment that leverages a virtual machine (VM) remotely, providing both security and flexibility.</description>
      <content:encoded><![CDATA[<p>Modern software development often requires us to juggle multiple services and environments. When you add the constraint of sensitive data or specific hardware requirements, this complexity can grow. Fortunately, tools like VSCode, Docker, and SSH are here to simplify our lives. In this post, we&rsquo;ll walk through setting up a development environment that leverages a virtual machine (VM) remotely, providing both security and flexibility.</p>
<h2 id="our-guidepost-a-django-application">Our Guidepost: A Django Application</h2>
<p>To better understand the concept, consider this example <code>docker-compose.yml</code> for a Django application, equipped with PostgreSQL as its database and Redis:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">`version</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;3.8&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">web</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">django:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">python manage.py runserver 0.0.0.0:8000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">app-data:/app</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8000:8000&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">db</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">postgres:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_DB</span><span class="p">:</span><span class="w"> </span><span class="l">sampledb</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_USER</span><span class="p">:</span><span class="w"> </span><span class="l">user</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">password</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">db-data:/var/lib/postgresql/data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cache</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">redis-server --save 20 1 --loglevel warning</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">app-data</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">db-data</span><span class="p">:</span><span class="w">
</span></span></span></code></pre></div><p>This configuration offers a Django web service (<code>web</code>) that leans on PostgreSQL (<code>db</code>) for its data storage and Redis (<code>cache</code>) for caching. It illustrates a typical setup that many web applications could use.</p>
<h2 id="why-ssh">Why SSH</h2>
<p>A virtual machine, named <code>DevVM</code> for this story, could be running in a cloud service, a secluded private data center, or even on a local grid. Whether you&rsquo;re working on an on-premise SecureMachine or a cloud-based DevVM, SSH is the key. It provides a secure bridge to both, ensuring encrypted access to these remote resources.</p>
<h2 id="containers-and-docker-compose">Containers and Docker Compose</h2>
<p>Containers have become instrumental in the modern software ecosystem due to their promise of portability. With many services now leaning towards containerized deployments, technologies like Docker, a popular choice among developers, ensure that applications are consistently executed, regardless of where they&rsquo;re run. To further enhance this, tools like Docker Compose step in, allowing us to weave multiple containers into an interdependent stack. This makes booting up an entire software stack, with all its intricacies and dependencies, as straightforward as executing a single command.</p>
<h2 id="setting-up-with-vscode">Setting Up with VSCode</h2>
<p>Now with the introduction out of the way, lets start explaining how to setup all this. I assume you have the following:</p>
<h3 id="prerequisites">Prerequisites</h3>
<ul>
<li>SSH key for  your <code>DevVM</code> or <code>SecureMachine</code></li>
<li>Docker installed on your local machine</li>
<li>Visual Studio Code (VSCode) with Docker extension installed on your local machine</li>
</ul>
<h3 id="step-1-clone-the-project">Step 1: Clone the Project</h3>
<p>We clone our project repository to our local machine:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone git@github.com:YourUsername/YourRepo.git
</span></span></code></pre></div><h3 id="step-2-connect-to-devvm-using-docker-and-ssh">Step 2: Connect to <code>DevVM</code> using Docker and SSH</h3>
<p>First, establish a Docker context for the VM:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker context create DevVM --docker <span class="s2">&#34;host=ssh://user@DevVM:22&#34;</span>
</span></span></code></pre></div><p>In VSCode:</p>
<ol>
<li>Open the Command Palette (<code>Ctrl + Shift + P</code>).</li>
<li>Type and select <code>Docker Contexts: Use</code>.</li>
<li>Choose the <code>DevVM</code> context.</li>
</ol>
<h3 id="step-3-configure-the-development-environment">Step 3: Configure the Development Environment</h3>
<p>In your project root, create a <code>.devcontainer</code> directory. Inside, add a <code>devcontainer.json</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;ProjectName&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;dockerComposeFile&#34;</span><span class="p">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">       <span class="s2">&#34;../docker-compose.yaml&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">],</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;service&#34;</span><span class="p">:</span> <span class="s2">&#34;web&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;workspaceFolder&#34;</span><span class="p">:</span> <span class="s2">&#34;/app/&#34;</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">   <span class="nt">&#34;overrideCommand&#34;</span><span class="p">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>In this case I copied my Django project inside the folder /app when building my docker and therefore, I target this folder as the startup <code>workspaceFolder</code>. Note the <code>overrideCommand: true</code>. I added this to override the default command that is specified in the docker-compose file, to avoid the container from starting up the Django server. By doing this it will fall back to the default command specified in the Dockerfile, which is <code>CMD [&quot;/bin/bash&quot;]</code>. This will allow us to start the Django server manually for debugging purposes.</p>
<p>To ensure data persistence and easy access, introduce a volume, <code>app-data</code>, to your docker service. In the context of our setup, this volume I attached to the <code>web</code> service, making sure that when your docker fails you won&rsquo;t lose your uncommited changes.</p>
<h3 id="step-4-engage-in-development">Step 4: Engage in Development</h3>
<p>In VSCode:</p>
<ol>
<li>Open the Command Palette.</li>
<li>Search and select &ldquo;Dev Containers: Reopen in Container.&rdquo;
OR</li>
<li>Click on the green bottom left <code>&gt;&lt;</code> button all the way in the corner.</li>
<li>Select &ldquo;Reopen in Container&rdquo;</li>
</ol>
<p>VSCode will now set everything up, and you&rsquo;re all set to develop!</p>
<h2 id="for-pycharm-aficionados">For PyCharm Aficionados</h2>
<p>I have to admit, I love PyCharm and I should definitely mention that it also supports connecting to Docker via SSH, allowing you to work seamlessly with your project using Docker Compose. However, when I tried this setup I had trouble setting up the Docker Compose interpreter via a remote ssh Docker service. Maybe this is a bug in the current 2023.2 or I&rsquo;m doing something wrong.</p>
<h2 id="for-the-solo-developer">For the solo developer</h2>
<p>You could also install VSCode Studio Server and setup a tunnel so you connect with your local VSC to it. I think it will make the setup a bit more smoother and less complex, however I choose for this solution since I had to share my machine. With this solution multiple developers can run a docker-stack on the same machine.</p>
<h2 id="wrapping-up">Wrapping Up</h2>
<p>Remote development doesn&rsquo;t have to be complex. With VSCode, Docker, and SSH, coupled with the power of VMs, developers can enjoy a flexible, secure, and consistent coding environment.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Millions of cells in Pathology Images: calculating cell density</title>
      <link>https://www.daangeijs.nl/posts/cell-counting-geopandas/</link>
      <pubDate>Wed, 27 Sep 2023 12:48:00 +0100</pubDate>
      
      <guid>https://www.daangeijs.nl/posts/cell-counting-geopandas/</guid>
      <description>We all know that digital pathology demands efficient handling of large-scale images. However, I hadn&amp;rsquo;t anticipated that the sheer volume of cell detections would significantly impact the performance of my cell density calculations within these images.  This article combines the ease of WholeSlideData, shapely, rasterio, and geopandas to achieve this in a fast and simple way.</description>
      <content:encoded><![CDATA[<p>We all know that digital pathology demands efficient handling of large-scale images. However, I hadn&rsquo;t anticipated that the sheer volume of cell detections would significantly impact the performance of my cell density calculations within these images.  This article combines the ease of WholeSlideData, shapely, rasterio, and geopandas to achieve this in a fast and simple way.</p>
<ul>
<li>
<p><a href="https://github.com/DIAGNijmegen/pathology-whole-slide-data"><strong>WholeSlideData:</strong></a> Amazing useful package made by Mart van Rijthoven for handling vast pathology images and annotations with multiple backends.</p>
</li>
<li>
<p><strong>Shapely and Rasterio:</strong> For converting image masks to polygonal representations.</p>
</li>
<li>
<p><strong>GeoPandas:</strong> For spatial operations, making the overlap calculations efficient.</p>
</li>
</ul>
<h2 id="loading-the-mask">Loading the Mask</h2>
<p>First lets load a mask that contains either tumor annotations or tumor predictions. In my case I used tumor predictions, made by one of my models.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1">#from wholeslidedata import WholeSlideImage</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">load_mask</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">spacing</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">mask_obj</span> <span class="o">=</span> <span class="n">WholeSlideImage</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">backend</span><span class="o">=</span><span class="s1">&#39;asap&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">mask</span> <span class="o">=</span> <span class="n">mask_obj</span><span class="o">.</span><span class="n">get_slide</span><span class="p">(</span><span class="n">spacing</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">mask</span><span class="o">.</span><span class="n">squeeze</span><span class="p">()</span>
</span></span></code></pre></div><p>In this method, a mask is loaded using the specific spacing. The function handles the nuances of image spacings and reads the mask at the desired resolution at once. The <code>wholeslidedata</code> package allows you to choose with what image backend you want to read the image. The default is OpenSlide, but in my case I use <a href="https://github.com/computationalpathologygroup/ASAP">ASAP</a> developed by <a href="https://geertlitjens.nl">Geert Litjens</a>.</p>
<h2 id="mask-to-polygon-conversion">Mask to Polygon Conversion</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1">#import shapely</span>
</span></span><span class="line"><span class="cl"><span class="c1">#from rasterio import features</span>
</span></span><span class="line"><span class="cl"><span class="c1">#import rasterio</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">mask_to_polygons_layer</span><span class="p">(</span><span class="n">mask</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">all_polygons</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">shape</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">features</span><span class="o">.</span><span class="n">shapes</span><span class="p">(</span><span class="n">mask</span><span class="o">.</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">int16</span><span class="p">),</span> <span class="n">mask</span><span class="o">=</span><span class="p">(</span><span class="n">mask</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                                        <span class="n">transform</span><span class="o">=</span><span class="n">rasterio</span><span class="o">.</span><span class="n">Affine</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)):</span>
</span></span><span class="line"><span class="cl">        <span class="n">all_polygons</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">shapely</span><span class="o">.</span><span class="n">geometry</span><span class="o">.</span><span class="n">shape</span><span class="p">(</span><span class="n">shape</span><span class="p">)</span><span class="o">.</span><span class="n">buffer</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">all_polygons</span> <span class="o">=</span> <span class="n">shapely</span><span class="o">.</span><span class="n">geometry</span><span class="o">.</span><span class="n">MultiPolygon</span><span class="p">(</span><span class="n">all_polygons</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">all_polygons</span>
</span></span></code></pre></div><p>This function converts a binary mask into a set of polygons using <code>rasterio</code> and <code>shapely</code>. This geometric representation is crucial for efficient spatial operations later. Okay, I admit it, its maybe hard to read  and it will not win a beauty award so let me guide you through this:
Here, the function iterates through the shapes detected in the binary mask.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">for</span> <span class="n">shape</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">features</span><span class="o">.</span><span class="n">shapes</span><span class="p">(</span><span class="n">mask</span><span class="o">.</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="o">.</span><span class="n">int16</span><span class="p">),</span> <span class="n">mask</span><span class="o">=</span><span class="p">(</span><span class="n">mask</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">                                    <span class="n">transform</span><span class="o">=</span><span class="n">rasterio</span><span class="o">.</span><span class="n">Affine</span><span class="p">(</span><span class="mf">1.0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">1.0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)):</span>
</span></span></code></pre></div><ul>
<li>
<p><code>mask.astype(np.int16)</code>: The mask, which is possibly in a boolean format, is converted into an integer format suitable for <code>features.shapes()</code>.</p>
</li>
<li>
<p><code>mask=(mask &gt; 0)</code>: This specifies that we are interested in the features where the mask values are greater than 0 (i.e., the foreground regions).</p>
</li>
<li>
<p><code>transform=rasterio.Affine(1.0, 0, 0, 0, 1.0, 0)</code>: This is an identity transformation. It means that there&rsquo;s no spatial transformation applied to the mask.</p>
</li>
</ul>
<p>Then for  each detected shape, the following steps happen:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">all_polygons</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">shapely</span><span class="o">.</span><span class="n">geometry</span><span class="o">.</span><span class="n">shape</span><span class="p">(</span><span class="n">shape</span><span class="p">)</span><span class="o">.</span><span class="n">buffer</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span>
</span></span></code></pre></div><ul>
<li>
<p><code>shapely.geometry.shape(shape)</code>: This converts the detected shape into a <code>shapely</code> polygon.</p>
</li>
<li>
<p><code>.buffer(0)</code>: The <code>buffer(0)</code> trick is a common technique in GIS operations. It&rsquo;s used here to fix potential topology errors in the polygons. Even though it might seem like it does nothing (since the buffer distance is 0), it can help in ensuring the created polygon is topologically valid.</p>
</li>
</ul>
<h2 id="computing-cell-density">Computing Cell Density</h2>
<p><code>GeoPandas</code> is an open-source Python library that makes working with geospatial data easier. While <code>pandas</code> is loved for data manipulation, it doesn’t have built-in capabilities to handle spatial data. <code>GeoPandas</code>, on the other hand, equips DataFrames with a geometry column, which can store spatial objects like points, lines, and polygons, allowing for sophisticated spatial operations. Just what I needed in this case!</p>
<p>Much like how <code>pandas</code> allows for efficient data operations due to its internal use of optimized C and Cython operations, <code>GeoPandas</code> spatial operations are vectorized, making spatial calculations very fast, also<code>GeoPandas</code> uses spatial indexing (via the R-tree data structure) for quick bounding box queries. When you’re trying to find overlaps between geometries (like in this case tumor polygon and cell points), spatial indexing ensures that you’re only comparing geometries that are close to one another, making overlap checks much quicker.</p>
<p>Lets get back to the problem and that was checking for million of cells if they where in the tumor area yes or no. GeoPandas was crucial and it took some time to figure it out, however as always, spending hours of time, the implementation was just a few lines of code. But before we start we first have to start with loading our cells.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1">#from wholeslidedata.annotation.wholeslideannotation import WholeSlideAnnotation</span>
</span></span><span class="line"><span class="cl"><span class="c1">#from wholeslidedata.annotation.callbacks import ScalingAnnotationCallback</span>
</span></span><span class="line"><span class="cl"><span class="n">scaler</span> <span class="o">=</span> <span class="n">ScalingAnnotationCallback</span><span class="p">(</span><span class="mi">1</span> <span class="o">/</span> <span class="mi">8</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">cells</span> <span class="o">=</span> <span class="n">WholeSlideAnnotation</span><span class="p">(</span><span class="n">cell_detections</span><span class="p">,</span> <span class="n">callbacks</span><span class="o">=</span><span class="p">(</span><span class="n">scaler</span><span class="p">,),</span> <span class="n">backend</span><span class="o">=</span><span class="s1">&#39;asap&#39;</span><span class="p">)</span>
</span></span></code></pre></div><p>The cell detections are in the annotation format that ASAP uses, so we can load them using the WholeSlideAnnotation class. For .xml files WholeSlideAnnotation uses ASAP as default backend. The <code>callbacks</code> argument is used to specify any preprocessing steps that need to be applied to the annotations. In this case, we need to scale the annotations to the same resolution as the tumor mask. In my case the cell detections in the .xml file are coordinates in the highest resolution, which is 0.25μm/pix. However, the tumor mask is at 2μm/pix. Therefore, we need to scale the coordinates by a factor of 8.</p>
<p>Now with our cell detections loaded we can breakdown the cell density calculation.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1">#import geopandas as gpd</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">cell_density</span><span class="p">(</span><span class="n">tumor_poly</span><span class="p">,</span> <span class="n">cells</span><span class="p">,</span> <span class="n">spacing</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">cell_points</span> <span class="o">=</span> <span class="p">[</span><span class="n">cell</span><span class="o">.</span><span class="n">_geometry</span> <span class="k">for</span> <span class="n">cell</span> <span class="ow">in</span> <span class="n">cells</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">a</span> <span class="o">=</span> <span class="n">gpd</span><span class="o">.</span><span class="n">GeoDataFrame</span><span class="p">(</span><span class="n">cell_points</span><span class="p">)</span><span class="o">.</span><span class="n">set_geometry</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">b</span> <span class="o">=</span> <span class="n">gpd</span><span class="o">.</span><span class="n">GeoDataFrame</span><span class="p">([</span><span class="n">tumor_poly</span><span class="p">])</span><span class="o">.</span><span class="n">set_geometry</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">check</span> <span class="o">=</span> <span class="n">gpd</span><span class="o">.</span><span class="n">tools</span><span class="o">.</span><span class="n">sjoin</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">how</span><span class="o">=</span><span class="s1">&#39;left&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">counts</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">check</span><span class="p">[</span><span class="s1">&#39;index_right&#39;</span><span class="p">]</span><span class="o">.</span><span class="n">notna</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">counts</span> <span class="o">/</span> <span class="p">(</span><span class="n">tumor_poly</span><span class="o">.</span><span class="n">area</span> <span class="o">*</span> <span class="n">spacing</span> <span class="o">*</span> <span class="mi">10</span> <span class="o">**</span> <span class="o">-</span><span class="mi">6</span><span class="p">)</span>
</span></span></code></pre></div><p>Here, the function converts tumor polygons and cell annotations into <code>GeoDataFrames</code>. The <code>sjoin</code> operation checks for overlaps between the two geometries. Without <code>GeoPandas</code>, you&rsquo;d potentially be comparing every cell point with every part of the tumor polygon—a massive computational task, especially for high-resolution images. However, with spatial indexing, only cells that have a chance to overlap with the tumor are compared, drastically reducing the number of computations. Also note the <code>set_geometry</code>  which was needed to designate a particular column as the &lsquo;geometry&rsquo; column. This column then becomes responsible for holding the spatial data (like points, lines, or polygons) that can be used in spatial operations. I called it <code>0</code> due to convenience and make sure to use the same name so sjoin knows which geometries to compare.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">check</span> <span class="o">=</span> <span class="n">gpd</span><span class="o">.</span><span class="n">tools</span><span class="o">.</span><span class="n">sjoin</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">how</span><span class="o">=</span><span class="s1">&#39;left&#39;</span><span class="p">)</span>
</span></span></code></pre></div><p>Left here means if a point from <code>a</code> lies within a polygon <code>b</code>, the resultant check DataFrame will have that point&rsquo;s data combined with the data from the polygon it lies within.
If a point from a doesn&rsquo;t lie within any polygon in b, that point will still appear in the check DataFrame, but the columns corresponding to b will be filled with NaN values.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">counts</span> <span class="o">=</span> <span class="nb">sum</span><span class="p">(</span><span class="n">check</span><span class="p">[</span><span class="s1">&#39;index_right&#39;</span><span class="p">]</span><span class="o">.</span><span class="n">notna</span><span class="p">())</span>
</span></span></code></pre></div><p>Therefore, when calculating the sum of cells that are not NaN, we get the number of cells that are within the tumor polygon. We get this data by selecting column &lsquo;index_right&rsquo; that contains the answers for each point.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">return</span> <span class="n">counts</span><span class="o">/</span><span class="p">(</span><span class="n">tumor_poly</span><span class="o">.</span><span class="n">area</span> <span class="o">*</span> <span class="n">spacing</span> <span class="o">*</span> <span class="mi">10</span> <span class="o">**</span> <span class="o">-</span><span class="mi">6</span><span class="p">)</span>
</span></span></code></pre></div><p>The purpose of the last line is to calculate and return the density of the points that are contained within the tumor polygon, in units of cells per square millimeter. Therefore we multiply with <code>10 ** -6</code> to convert the pixel spacing that has square micrometers (μm²) to square millimeters (mm²), so we end up with the metric cells/mm² tumor.</p>
<h2 id="everything-put-together">Everything put together</h2>
<p>Want to see the full code? Check out this repository where I have put together an example notebook and normal python script and also included some example data.</p>
<div class="github-card">
    <h2><a href="https://github.com/daangeijs/pathology-cell-density" target="_blank">pathology-cell-density</a></h2>
    <p>A demonstration script for analyzing cell density in whole slide images (WSIs).</p>
    <div class="card-details">
        <span class="language">Jupyter Notebook</span>
        <span class="stars">⭐ 1</span>
        <span class="forks">🍴 0</span>
    </div>
</div>
]]></content:encoded>
    </item>
    
    <item>
      <title>Website Analytics with Umami, Netlify and a self-hosted database. </title>
      <link>https://www.daangeijs.nl/posts/umami-netlify/</link>
      <pubDate>Tue, 22 Aug 2023 12:48:00 +0100</pubDate>
      
      <guid>https://www.daangeijs.nl/posts/umami-netlify/</guid>
      <description>Umami is a sleek, open-source analytics tool that provides an alternative to mainstream solutions like Google Analytics. Its simplicity and transparency make it a preferred choice for those wary of the intricacies and potential privacy concerns associated with bigger platforms.</description>
      <content:encoded><![CDATA[<p><figure>
    <img loading="lazy" src="cover.jpg"/> 
</figure>

Umami is a sleek, open-source analytics tool that provides an alternative to mainstream solutions like Google Analytics. Its simplicity and transparency make it a preferred choice for those wary of the intricacies and potential privacy concerns associated with bigger platforms.</p>
<p>In this case I like having autonomy and control of hosting my own data, but I also appreciate the efficiency and scalability of cloud services. That&rsquo;s why, I&rsquo;ve opted to host the database needed for Umami at home on my homeserver using Proxmox.  However, when it comes to the dashboard – the visual heart of Umami – I use Netlify. Hosting the Umami dashboard on Netlify not only offloads my trusty NUC from running yet another service but also made installation a breeze with Netlify&rsquo;s effortless deployment process. In this article I will show you how I set it up.</p>
<h2 id="setting-up-database-on-proxmox">Setting Up Database on Proxmox</h2>
<ol>
<li>
<p><strong>Download CT Template</strong>: Begin by obtaining the <code>turnkey-postgresql</code> CT template. The easiest way to do this is to download the template from the Proxmox web interface. Navigate to &ldquo;Templates&rdquo; -&gt; &ldquo;Download&rdquo; and search for &ldquo;turnkey-postgresql&rdquo;. Select the template and click &ldquo;Download&rdquo;.</p>
</li>
<li>
<p><strong>Create a LXC container</strong>:
<figure>
    <img loading="lazy" src="1.png"/> 
</figure>
</p>
<ul>
<li>Assign a fixed IP address (fixed to enable port forwarding).</li>
<li>Allocate 1024MB memory and 1024MB swap.</li>
<li>Dedicate 1 CPU core.</li>
<li>Designate 16GB for disk storage.</li>
<li>Ensure &ldquo;start after created&rdquo; is selected.</li>
</ul>
</li>
</ol>
<p>For me these where the resources that I had available, but you can adjust these to your own needs.</p>
<ol start="3">
<li><strong>Complete Initialization</strong>: Access the console of your started container, log in with <code>root</code> and the password you set up at the previous step. Complete the installation, skipping any unnecessary add-ons but making sure to apply the updates.
<figure>
    <img loading="lazy" src="3.png"/> 
</figure>
</li>
<li><strong>Database Setup</strong>: Navigate to the browser using assigned-static-IP-address:12322 or simply input the IP address. This will take you to a dashboard where you can select Adminer. Use Adminer to log in with PostgreSQL credentials. Create a table named <code>umami</code>.
<figure>
    <img loading="lazy" src="5.png"/> 
</figure>
</li>
<li><strong>Port forwarding</strong> Make sure you don&rsquo;t forget to enable port forwarding (5432) to the IP address of your running Postgres container. Ofcourse, this completely depends on your network setup. Keep in mind that exposing a port does come with security vulnerabilities. If you have an Ubiquiti router <a href="/posts/ubiquiti-vlan/">you can read this article</a> for more information on how to set this up in a more safe way.</li>
</ol>
<h2 id="deploying-umami-on-netlify">Deploying Umami on Netlify</h2>
<ol>
<li>
<p><strong>Fork Repository</strong>: Fork the Umami repository to your GitHub account:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl">https://github.com/umami-software/umami
</span></span></code></pre></div></li>
<li>
<p><strong>Netlify Setup</strong>:</p>
<ul>
<li>Log into Netlify.</li>
<li>Choose &ldquo;Add New Site&rdquo; -&gt; &ldquo;Import Existing Site&rdquo;.</li>
<li>Opt for &ldquo;Deploy with GitHub&rdquo;.</li>
<li>Select your forked Umami repository.</li>
</ul>
</li>
<li>
<p><strong>Environment Variable</strong>:
<figure>
    <img loading="lazy" src="6.png"/> 
</figure>
</p>
<ul>
<li>In the site settings of your new project, navigate to &ldquo;Site configuration&rdquo; -&gt; &ldquo;Environment variables&rdquo;.</li>
<li>Add the <code>DATABASE_URL</code> variable with the value:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl">postgresql://<span class="p">&lt;</span><span class="nt">postgres_account</span><span class="p">&gt;</span>:<span class="p">&lt;</span><span class="nt">postgress_password</span><span class="p">&gt;</span>@<span class="p">&lt;</span><span class="nt">your_db_ip</span><span class="p">&gt;</span>/umami
</span></span></code></pre></div></li>
</ul>
</li>
<li>
<p><strong>Trigger Deployment</strong>:
<figure>
    <img loading="lazy" src="7.png"/> 
</figure>
</p>
<ul>
<li>In the Netlify dashboard, go to &ldquo;Deploys&rdquo;.</li>
<li>Select &ldquo;Trigger Deploy&rdquo; and choose &ldquo;Clear cache and deploy site&rdquo;.</li>
</ul>
</li>
</ol>
<p>The example here is just with Proxmox, but you can choose any popular solutions like AWS, Azure, DigitalOcean, or Heroku— provided they support PostgreSQL.  At the end you just need to update the URL in the Netlify dashboard.  Choose what&rsquo;s best for your needs.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Journey to Optimized Images: Hugo, Decap CMS, and Page Bundles</title>
      <link>https://www.daangeijs.nl/posts/hugo-papermodx-optimize/</link>
      <pubDate>Wed, 09 Aug 2023 21:48:00 +0100</pubDate>
      
      <guid>https://www.daangeijs.nl/posts/hugo-papermodx-optimize/</guid>
      <description>This turned out to be an unexpected long journey. It all began when I realized that my &lt;a href=&#34;https://dekeukenvandael.nl&#34;&gt;website&lt;/a&gt; images weren&amp;rsquo;t optimized as I had expected. I had been relying on the PaperMod and PaperModX themes in Hugo to handle image optimization. Everything seemed to be in place, but my images were still bulky and slow to load. The problem was evident, but the cause? Not so much.</description>
      <content:encoded><![CDATA[<p>This turned out to be an unexpected long journey. It all began when I realized that my <a href="https://dekeukenvandael.nl">website</a> images weren&rsquo;t optimized as I had expected. I had been relying on the PaperMod and PaperModX themes in Hugo to handle image optimization. Everything seemed to be in place, but my images were still bulky and slow to load. The problem was evident, but the cause? Not so much.</p>
<p>Digging into the problem, I started by examining the <code>cover.html</code> file in the theme. This file is crucial for handling images. As I skimmed through the lines, an interesting section of the code caught my attention:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="err">##</span> <span class="nx">themes</span><span class="o">/</span><span class="nx">PaperMod</span><span class="o">/</span><span class="nx">layouts</span><span class="o">/</span><span class="nx">partials</span><span class="o">/</span><span class="nx">cover</span><span class="p">.</span><span class="nx">html</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="o">-</span> <span class="k">if</span> <span class="err">$</span><span class="nx">cover</span> <span class="o">-</span><span class="p">}}{{</span><span class="cm">/* i.e it is present in page bundle */</span><span class="p">}}</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span><span class="p">.</span><span class="nx">optimisation</span> <span class="nx">code</span> <span class="nx">here</span>
</span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="o">-</span> <span class="k">else</span> <span class="p">}}{{</span><span class="cm">/* For absolute urls and external links, no img processing here */</span><span class="p">}}</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span><span class="p">.</span> <span class="nx">well</span><span class="o">...</span><span class="nx">do</span> <span class="nx">nothing</span>
</span></span></code></pre></div><p>The theme was checking for images within something called a &ldquo;page bundle.&rdquo; If the image wasn&rsquo;t within this bundle, optimization processes wouldn&rsquo;t be triggered. That was my &ldquo;Aha!&rdquo; moment. For the themes to work their magic, my content needed to be organized into Hugo&rsquo;s page bundles.</p>
<p>So, with newfound clarity, I set about restructuring my content into these page bundles. The results? When building Hugo locally, the images were automatically resized and optimized, all thanks to the wonders of my new page bundles!</p>
<p>When I transitioned to page bundles, I anticipated that my <code>config.yml</code> for the CMS would require some adjustments. Luckily the CMS supported Page bundles by using so called <code>collections</code> and I followed the <a href="https://decapcms.org/docs/collection-types/">official documentation</a> making the adjustments needed.</p>
<p>However, with every solution came a new challenge. Now that my content was structured the right way for image optimization, my newly configured Decap CMS (previously Netlify CMS) threw a tantrum. Suddenly, it stopped listing any of my articles.  This was the most timeconsuming and puzzling setback.</p>
<p>I scoured the internet for answers, hoping to stumble upon someone who had faced a similar issue. And then, I came across a post on <a href="https://blog.millerti.me/2021/12/23/supporting-hugo-page-bundles-in-netlify-cms/">millerti.me</a> that held the missing piece to my puzzle. The article mentioned the specific lines I needed to integrate in the collections:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c"># Support Hugo page bundles that puts index.md and images in folders named by slug</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{slug}}/index&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">media_folder</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;&#34;</span><span class="w">
</span></span></span></code></pre></div><p>Adding these lines was the magic touch. Now my CMS seamlessly stored new content, and it also listed my previous articles again and everything worked smoothly. This all resulted in the following config.yml:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="c">## admin/config.yml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">backend</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">git-gateway</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">branch</span><span class="p">:</span><span class="w"> </span><span class="l">main</span><span class="w"> </span><span class="c"># Branch to update (optional; defaults to master)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">site_url</span><span class="p">:</span><span class="w"> </span><span class="l">https://dekeukenvandael.nl</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">logo_url</span><span class="p">:</span><span class="w"> </span><span class="l">https://www.dekeukenvandael.nl/images/static/logo.svg</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">publish_mode</span><span class="p">:</span><span class="w"> </span><span class="l">editorial_workflow</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">media_folder</span><span class="p">:</span><span class="w"> </span><span class="l">static/images</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">public_folder</span><span class="p">:</span><span class="w"> </span><span class="l">/images</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">collections</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;recipes&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">label</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Recipes&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">label_singular</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Recipe&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">folder</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;content/recipes&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">create</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Support Hugo page bundles that puts index.md and images in folders named by slug</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{year}}-{{month}}-{{day}}-{{slug}}/index&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="c"># Note the usage of title here instead of slug</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">preview_path</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;recipes/{{year}}-{{month}}-{{day}}-{{title}}&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">preview_path_date_field</span><span class="p">:</span><span class="w"> </span><span class="l">date</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">filter</span><span class="p">:</span><span class="w"> </span>{<span class="nt">field</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;ignore&#34;</span><span class="nt">, value</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">editor</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">preview</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">fields</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- {<span class="w"> </span><span class="nt">label: &#39;Title&#39;, name: &#39;title&#39;, widget</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;string&#39;</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- {<span class="w"> </span><span class="nt">label: &#39;Publish Date&#39;, name: &#39;date&#39;, widget</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;datetime&#39;</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- {<span class="w"> </span><span class="nt">label</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Authors&#34;</span><span class="nt">, name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;author&#34;</span><span class="nt">, widget</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;list&#34;</span><span class="nt">, summary</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;{{fields.author_name}}&#34;</span><span class="nt">, field</span><span class="p">:</span><span class="w"> </span>{<span class="w"> </span><span class="nt">label</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Author name&#34;</span><span class="nt">, name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;author_name&#34;</span><span class="nt">, widget</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;string&#34;</span><span class="w"> </span>}}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- {<span class="w"> </span><span class="nt">label: &#39;Tags&#39;, name: &#39;tags&#39;, widget: &#39;list&#39;, required: false, items</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;string&#39;</span><span class="w"> </span>}<span class="p">]</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- {<span class="w"> </span><span class="nt">label: &#39;Categories&#39;, name: &#39;categories&#39;, widget: &#39;list&#39;, required: false, items</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>{<span class="w"> </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;string&#39;</span><span class="w"> </span>}<span class="p">]</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- {<span class="w"> </span><span class="nt">label: &#39;Cover Image&#39;, name: &#39;cover&#39;, widget: &#39;object&#39;, fields</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span>{<span class="w"> </span><span class="nt">label: &#39;Image&#39;, name: &#39;image&#39;, required: false, widget</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;image&#39;</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- {<span class="w"> </span><span class="nt">label: &#39;Summary&#39;, name: &#39;summary&#39;, widget: &#39;text&#39;, required</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w"> </span>}<span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- {<span class="w"> </span><span class="nt">label: &#39;Body&#39;, name: &#39;body&#39;, widget</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;markdown&#39;</span><span class="w"> </span>}<span class="w">
</span></span></span></code></pre></div><p>I hope it helps to reduce the time and effort for others facing similar issues. It wasn&rsquo;t a straightforward process for me, and I&rsquo;d be pleased if others can benefit from the lessons I learned. Maybe with the time you saved you can write an nice article about how this helped you ;).</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Enhancing Your Hugo Blog: Embedding GitHub Repositories</title>
      <link>https://www.daangeijs.nl/posts/github-partials-shortcodes/</link>
      <pubDate>Tue, 25 Jul 2023 12:48:00 +0100</pubDate>
      
      <guid>https://www.daangeijs.nl/posts/github-partials-shortcodes/</guid>
      <description>I code a lot, and GitHub is where I share my work. But I wanted to take it a step further and feature my projects on my own website, as you can see on my main page. If you&amp;rsquo;re in the same boat and want to showcase your GitHub repositories on your Hugo blog, have a look at this article. We&amp;rsquo;ll explore two straightforward ways to make it happen.</description>
      <content:encoded><![CDATA[<p>I code a lot, and GitHub is where I share my work. But I wanted to take it a step further and feature my projects on my own website, as you can see on my main <a href="https://daangeijs.nl">page</a>. If you&rsquo;re in the same boat and want to showcase your GitHub repositories on your Hugo blog, have a look at this article. We&rsquo;ll explore two straightforward ways to make it happen.</p>
<ol>
<li><strong>Shortcodes</strong>: For manually embedding specific repositories in markdown.</li>
<li><strong>Partials with Python</strong>: For automatically fetching and displaying selected repositories using a Python script.</li>
</ol>
<h2 id="1-embedding-a-github-repo-using-shortcodes">1. Embedding a GitHub Repo Using Shortcodes</h2>
<h3 id="styling-with-css">Styling with CSS:</h3>
<p>For a consistent appearance between shortcodes and partials, we&rsquo;re employing a basic CSS design. Here&rsquo;s a style snippet that you can add to your site&rsquo;s CSS file:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="p">.</span><span class="nc">github-card</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">border</span><span class="p">:</span> <span class="mi">1</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#e1e4e8</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">padding</span><span class="p">:</span> <span class="mi">15</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">margin</span><span class="p">:</span> <span class="mi">20</span><span class="kt">px</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">border-radius</span><span class="p">:</span> <span class="mi">5</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">box-shadow</span><span class="p">:</span> <span class="mi">0</span> <span class="mi">1</span><span class="kt">px</span> <span class="mi">3</span><span class="kt">px</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mf">0.12</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">background</span><span class="p">:</span> <span class="mh">#f6f8fa</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nc">github-card</span> <span class="nt">h2</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">font-size</span><span class="p">:</span> <span class="mf">1.2</span><span class="kt">em</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">margin-bottom</span><span class="p">:</span> <span class="mi">8</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">margin</span><span class="p">:</span> <span class="kc">auto</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nc">github-card</span> <span class="nt">h2</span> <span class="nt">a</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">text-decoration</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#0366d6</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nc">github-card</span> <span class="nt">p</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">margin-bottom</span><span class="p">:</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nc">card-details</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">display</span><span class="p">:</span> <span class="kc">flex</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">gap</span><span class="p">:</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">font-size</span><span class="p">:</span> <span class="mf">0.9</span><span class="kt">em</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nc">language</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#586069</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nc">stars</span><span class="o">,</span> <span class="p">.</span><span class="nc">forks</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">color</span><span class="p">:</span> <span class="mh">#586069</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>This CSS ensures a neat card-style presentation for the GitHub repositories. Both our shortcode and our partial will utilize this styling.</p>
<h3 id="the-shortcode">The Shortcode:</h3>
<p>Now, let&rsquo;s create a new shortcode. In your Hugo site&rsquo;s <code>layouts/shortcodes</code> directory, create a file called <code>githubRepoCard.html</code> and paste the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;github-repo-card&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;{{ .Get &#34;</span><span class="na">url</span><span class="err">&#34;</span> <span class="err">}}&#34;</span> <span class="na">target</span><span class="o">=</span><span class="s">&#34;_blank&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="nt">h3</span><span class="p">&gt;</span>{{ .Get &#34;name&#34; }}<span class="p">&lt;/</span><span class="nt">h3</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>{{ .Get &#34;description&#34; }}<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>Language:<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> {{ .Get &#34;language&#34; }}<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>Stars:<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> {{ .Get &#34;stars&#34; }}<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>Forks:<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> {{ .Get &#34;forks&#34; }}<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span></code></pre></div><h3 id="using-the-shortcode">Using the Shortcode:</h3>
<p>In any markdown file, you can now use the shortcode like this (note the space I added between {{ to prevent hugo from rendering the shortcode on this page):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">{ {<span class="p">&lt;</span> <span class="nt">githubRepoCard</span> <span class="na">url</span><span class="o">=</span><span class="s">&#34;https://github.com/username/myrepo&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;MyRepo&#34;</span> <span class="na">description</span><span class="o">=</span><span class="s">&#34;This is my awesome repo&#34;</span> <span class="na">language</span><span class="o">=</span><span class="s">&#34;JavaScript&#34;</span> <span class="na">stars</span><span class="o">=</span><span class="s">&#34;100&#34;</span> <span class="na">forks</span><span class="o">=</span><span class="s">&#34;50&#34;</span> <span class="p">&gt;</span>}}
</span></span></code></pre></div><h2 id="2-auto-generating-a-partial-with-python">2. Auto-generating a Partial with Python</h2>
<h3 id="the-partial">The Partial:</h3>
<p>Similair to the shortcode we now make a partial called <code>githubRepoCard.html</code> and place it in our Hugo site&rsquo;s <code>layouts/partials</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;github-repo-card&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">a</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;{{ .url }}&#34;</span> <span class="na">target</span><span class="o">=</span><span class="s">&#34;_blank&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="nt">h3</span><span class="p">&gt;</span>{{ .name }}<span class="p">&lt;/</span><span class="nt">h3</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">a</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>{{ .description }}<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>Language:<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> {{ .language }}<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>Stars:<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> {{ .stars }}<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>Forks:<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> {{ .forks }}<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;&lt;</span><span class="nt">strong</span><span class="p">&gt;</span>Last Updated:<span class="p">&lt;/</span><span class="nt">strong</span><span class="p">&gt;</span> {{ .lastUpdated }}<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span></code></pre></div><h3 id="the-python-script">The Python Script:</h3>
<p>The Python script is designed to scrape your GitHub profile and retrieve information about specific repositories. I use the <code>BeautifulSoup</code> library for this, which made parsing HTML and navigating the DOM pretty easy.</p>
<p><strong>1. Setting Up</strong></p>
<p>Before diving into the script, ensure you have the necessary libraries installed:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install requests beautifulsoup4
</span></span></code></pre></div><p><strong>2. Initialize Constants and Fetch Profile Page</strong></p>
<p>Start by setting up the base URL and headers. The headers, especially the &lsquo;User-Agent&rsquo;, help in avoiding blocks when making requests to GitHub.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">URL</span> <span class="o">=</span> <span class="s2">&#34;https://github.com/daangeijs?tab=repositories&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">HEADERS</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;User-Agent&#39;</span><span class="p">:</span> <span class="s1">&#39;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3&#39;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Then, fetch the repos from the main profile page, I found the information for each in the div with the class <code>col-10 col-lg-9 d-inline-block</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">BASE_URL</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">HEADERS</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">soup</span> <span class="o">=</span> <span class="n">BeautifulSoup</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">content</span><span class="p">,</span> <span class="s1">&#39;html.parser&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">repos</span> <span class="o">=</span> <span class="n">soup</span><span class="o">.</span><span class="n">find_all</span><span class="p">(</span><span class="s1">&#39;div&#39;</span><span class="p">,</span> <span class="n">class_</span><span class="o">=</span><span class="s1">&#39;col-10 col-lg-9 d-inline-block&#39;</span><span class="p">)</span>
</span></span></code></pre></div><p><strong>3. Filtering Repositories</strong></p>
<p>Since I don&rsquo;t want to list all my repos on the website I only collect a subset of repositories. To do this, I create a list of repository names that I want to include:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">specified_repos</span> <span class="o">=</span> <span class="p">[</span><span class="s1">&#39;daangeijs.nl&#39;</span><span class="p">,</span> <span class="s1">&#39;dekeukenvandael.nl&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="n">partials</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl"><span class="n">nl</span> <span class="o">=</span> <span class="s1">&#39;</span><span class="se">\n</span><span class="s1">&#39;</span> <span class="c1"># Newline character since it is not possible to use newlines in formatted strings</span>
</span></span></code></pre></div><p><strong>4. Loop through Repositories</strong></p>
<p>Now, for each repository on the profile page, check if its name matches any of the names in our <code>specified_repos</code> list. If there&rsquo;s a match, extract additional information:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">for</span> <span class="n">repo</span> <span class="ow">in</span> <span class="n">repos</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">name</span> <span class="o">=</span> <span class="n">repo</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s1">&#39;a&#39;</span><span class="p">,</span> <span class="n">itemprop</span><span class="o">=</span><span class="s1">&#39;name codeRepository&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">text</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">specified_repos</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="o">...</span>
</span></span></code></pre></div><p><strong>5. Dive Deeper into Each Repo</strong></p>
<p>For more detailed data, like the exact number of stars and forks, we need to visit each repo&rsquo;s individual page, here you see how I extracted the main project language, description and last updated time:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Extract base info  </span>
</span></span><span class="line"><span class="cl"><span class="n">repo_url</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;https://github.com</span><span class="si">{</span><span class="n">repo</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s1">&#39;a&#39;</span><span class="p">,</span> <span class="n">itemprop</span><span class="o">=</span><span class="s1">&#39;name codeRepository&#39;</span><span class="p">)[</span><span class="s1">&#39;href&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span>  
</span></span><span class="line"><span class="cl"><span class="n">description_elem</span> <span class="o">=</span> <span class="n">repo</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s1">&#39;p&#39;</span><span class="p">,</span> <span class="n">itemprop</span><span class="o">=</span><span class="s1">&#39;description&#39;</span><span class="p">)</span>  
</span></span><span class="line"><span class="cl"><span class="n">description</span> <span class="o">=</span> <span class="n">description_elem</span><span class="o">.</span><span class="n">text</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">if</span> <span class="n">description_elem</span> <span class="k">else</span> <span class="s2">&#34;No description provided.&#34;</span>  
</span></span><span class="line"><span class="cl"><span class="n">language_elem</span> <span class="o">=</span> <span class="n">repo</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s1">&#39;span&#39;</span><span class="p">,</span> <span class="n">itemprop</span><span class="o">=</span><span class="s1">&#39;programmingLanguage&#39;</span><span class="p">)</span>  
</span></span><span class="line"><span class="cl"><span class="n">language</span> <span class="o">=</span> <span class="n">language_elem</span><span class="o">.</span><span class="n">text</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">if</span> <span class="n">language_elem</span> <span class="k">else</span> <span class="s2">&#34;Not specified&#34;</span>  
</span></span><span class="line"><span class="cl"><span class="n">last_updated</span> <span class="o">=</span> <span class="n">repo</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s1">&#39;relative-time&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">attrs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;datetime&#39;</span><span class="p">,</span> <span class="s1">&#39;Unknown Date&#39;</span><span class="p">)</span>
</span></span></code></pre></div><p><strong>6. Extract Stars, Forks, and Last Updated Time</strong></p>
<p>Using these specified HTML locations we extract the number of stars and forks:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Fetch repo-specific page to extract stars and forks  </span>
</span></span><span class="line"><span class="cl"><span class="n">repo_response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">repo_url</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">HEADERS</span><span class="p">)</span>  
</span></span><span class="line"><span class="cl"><span class="n">repo_soup</span> <span class="o">=</span> <span class="n">BeautifulSoup</span><span class="p">(</span><span class="n">repo_response</span><span class="o">.</span><span class="n">content</span><span class="p">,</span> <span class="s1">&#39;html.parser&#39;</span><span class="p">)</span>  
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl"><span class="n">stars_elem</span> <span class="o">=</span> <span class="n">repo_soup</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s1">&#39;span&#39;</span><span class="p">,</span> <span class="nb">id</span><span class="o">=</span><span class="s2">&#34;repo-stars-counter-star&#34;</span><span class="p">)</span>  
</span></span><span class="line"><span class="cl"><span class="n">forks_elem</span> <span class="o">=</span> <span class="n">repo_soup</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s1">&#39;span&#39;</span><span class="p">,</span> <span class="nb">id</span><span class="o">=</span><span class="s2">&#34;repo-network-counter&#34;</span><span class="p">)</span>  
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl"><span class="n">stars</span> <span class="o">=</span> <span class="n">stars_elem</span><span class="p">[</span><span class="s1">&#39;title&#39;</span><span class="p">]</span> <span class="k">if</span> <span class="n">stars_elem</span> <span class="ow">and</span> <span class="s1">&#39;title&#39;</span> <span class="ow">in</span> <span class="n">stars_elem</span><span class="o">.</span><span class="n">attrs</span> <span class="k">else</span> <span class="s1">&#39;0&#39;</span>  
</span></span><span class="line"><span class="cl"><span class="n">forks</span> <span class="o">=</span> <span class="n">forks_elem</span><span class="p">[</span><span class="s1">&#39;title&#39;</span><span class="p">]</span> <span class="k">if</span> <span class="n">forks_elem</span> <span class="ow">and</span> <span class="s1">&#39;title&#39;</span> <span class="ow">in</span> <span class="n">forks_elem</span><span class="o">.</span><span class="n">attrs</span> <span class="k">else</span> <span class="s1">&#39;0&#39;</span>
</span></span></code></pre></div><p>We then generate the partial string that we add to a list</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">partial</span> <span class="o">=</span> <span class="sa">f</span><span class="s1">&#39;&#39;&#39;</span><span class="se">{{{{</span><span class="s1"> partial &#34;githubRepoCard.html&#34; (dict &#34;url&#34; &#34;</span><span class="si">{</span><span class="n">repo_url</span><span class="si">}</span><span class="s1">&#34; &#34;name&#34; &#34;</span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s1">&#34; &#34;description&#34; &#34;</span><span class="si">{</span><span class="n">description</span><span class="si">}</span><span class="s1">&#34; &#34;language&#34; &#34;</span><span class="si">{</span><span class="n">language</span><span class="si">}</span><span class="s1">&#34; &#34;stars&#34; &#34;</span><span class="si">{</span><span class="n">stars</span><span class="si">}</span><span class="s1">&#34; &#34;forks&#34; &#34;</span><span class="si">{</span><span class="n">forks</span><span class="si">}</span><span class="s1">&#34; &#34;lastUpdated&#34; &#34;</span><span class="si">{</span><span class="n">last_updated</span><span class="si">}</span><span class="s1">&#34;) </span><span class="se">}}}}</span><span class="s1">&#39;&#39;&#39;</span>  
</span></span><span class="line"><span class="cl"><span class="n">partials</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">partial</span><span class="p">)</span>
</span></span></code></pre></div><p><strong>7. Save Extracted Data as Hugo Partials</strong></p>
<p>Once all the desired data is scraped and the partials are generated we save them in a file and we add a little bit of html to create a div and title around the partials:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">wrapped_content</span> <span class="o">=</span> <span class="sa">f</span><span class="s1">&#39;&#39;&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">&lt;div class=&#34;latest-publications&#34;&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">    &lt;h2&gt;My GitHub repositories&lt;/h2&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">    </span><span class="si">{</span><span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">partials</span><span class="p">)</span><span class="si">}</span><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">&lt;/div&gt;
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;&#39;&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s2">&#34;partials_generated.html&#34;</span><span class="p">,</span> <span class="s2">&#34;w&#34;</span><span class="p">)</span> <span class="k">as</span> <span class="n">file</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">file</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">wrapped_content</span><span class="p">)</span>
</span></span></code></pre></div><hr>
<h3 id="using-the-generated-html">Using the Generated HTML:</h3>
<p>Once you run the Python script, it&rsquo;ll create an HTML file named <code>partials_generated.html</code> in your directory. This file contains wrapped partial calls, which you can include in your Hugo layouts or content. I store mine here: <code>layouts/partials/github.html</code></p>
<p>To add the partial to a page simply use this in your html template:
<code>{{ partial &quot;github.html&quot; }}</code></p>
<hr>
<p>So that is it, two ways I implemented partials and shortcodes to add my GitHub repo&rsquo;s to this Hugo generated website. Here the final result together with the already existing gist shortcode from Hugo. Stacked on each other it looks great! For the latest version have a look at the source of my website. Or you can copy the code for the python script below:</p>
<div class="github-card">
    <h2><a href="https://github.com/daangeijs/daangeijs.nl" target="_blank">daangeijs.nl</a></h2>
    <p>This repo contains the sourcecode of the website https://daangeijs.nl/, my personal website. It is built using Hugo, a static site generator. The theme is based on PapermodX.</p>
    <div class="card-details">
        <span class="language">HTML</span>
        <span class="stars">⭐ 1</span>
        <span class="forks">🍴 0</span>
    </div>
</div>
<script type="application/javascript" src="https://gist.github.com/daangeijs/e00759f976d6d8df96a89761d772ee2d.js"></script>

]]></content:encoded>
    </item>
    
    <item>
      <title>Setting Up an Isolated Virtual Server in a VLAN on Ubiquiti and Proxmox</title>
      <link>https://www.daangeijs.nl/posts/ubiquiti-vlan/</link>
      <pubDate>Wed, 14 Jun 2023 12:48:00 +0100</pubDate>
      
      <guid>https://www.daangeijs.nl/posts/ubiquiti-vlan/</guid>
      <description>Hosting your own services to be accessed by the public internet comes with its share of challenges, especially when exposing ports security concerns are involved. Cloud hosting providers are a good way to solve some of these problems, but can the costs can rise pretty quickly.</description>
      <content:encoded><![CDATA[<p>Hosting your own services to be accessed by the public internet comes with its share of challenges, especially when exposing ports security concerns are involved. Cloud hosting providers are a good way to solve some of these problems, but can the costs can rise pretty quickly.</p>
<p>In this article I wrote down a  step-by-step walkthrough on creating an isolated environment for hosting services, using a VLAN setup.  By using a VLAN and setting up a firewall we can isolate the virtual server from your primary private network, layering an additional shield of security to your setup. As an example we&rsquo;ll use a virtual machine (VM) hosting a database service as our primary example.</p>
<h3 id="ubiquiti-vlan-configuration">Ubiquiti VLAN Configuration</h3>
<h4 id="1-create-a-vlan">1. Create a VLAN</h4>
<ul>
<li>Log into your <strong>UniFi Controller</strong>.</li>
<li>Navigate to the &ldquo;Settings&rdquo; (gear icon) at the bottom left.</li>
<li>Under &ldquo;Networks&rdquo;, click on &ldquo;Create New Network&rdquo;.</li>
<li>Provide a name for the network, for instance, &ldquo;Public VLAN&rdquo;.</li>
<li>Set &ldquo;Purpose&rdquo; to &ldquo;Corporate&rdquo;.</li>
<li>Assign a VLAN ID of &ldquo;10&rdquo;.</li>
<li>Define the subnet as <code>10.0.0.1/24</code>.</li>
<li>Configure the DHCP range if required and save these settings.</li>
</ul>
<h4 id="2-firewall-rules-for-vlan-traffic">2. Firewall Rules for VLAN Traffic</h4>
<ul>
<li>
<p>Proceed to &ldquo;Routing &amp; Firewall&rdquo; within the settings.</p>
</li>
<li>
<p>Select &ldquo;Firewall&rdquo; and then &ldquo;LAN IN&rdquo;.</p>
</li>
<li>
<p>Set up a rule that permits only PostgreSQL traffic:</p>
<ul>
<li>Name: <strong>Allow PostgreSQL to WAN</strong></li>
<li>Action: <strong>Accept</strong></li>
<li>Source: <strong>Public VLAN</strong></li>
<li>Destination: <strong>Any</strong></li>
<li>Ports: <strong>5432</strong> (PostgreSQL&rsquo;s default port)</li>
</ul>
</li>
<li>
<p>Create rules that block all traffic from the VLAN to other local networks:</p>
<ul>
<li>Name: <strong>Block VLAN to all LANs</strong></li>
<li>Action: <strong>Drop</strong></li>
<li>Source: <strong>Public VLAN</strong></li>
<li>Destination: <strong>All other local networks/VLANs</strong></li>
</ul>
</li>
</ul>
<h4 id="3-port-forwarding">3. Port Forwarding</h4>
<ul>
<li>
<p>Navigate to &ldquo;Routing &amp; Firewall&rdquo; and select &ldquo;Port Forwarding&rdquo;.</p>
</li>
<li>
<p>Click on the &ldquo;+ Create New Rule&rdquo; or &ldquo;Add New Port Forward Rule&rdquo; button, which should open a new window or pane for rule creation.</p>
</li>
<li>
<p><strong>Name</strong>: Give the rule a descriptive name, e.g., &ldquo;PostgreSQL Remote Access&rdquo;.</p>
</li>
<li>
<p><strong>Enabled</strong>: Make sure this is toggled on.</p>
</li>
<li>
<p><strong>Rule Applied</strong>: Set to &ldquo;After Predefined Rules&rdquo;</p>
</li>
<li>
<p><strong>WAN Interface</strong>: Usually set to &ldquo;All&rdquo; unless you have multiple WANs and prefer a specific one.</p>
</li>
<li>
<p><strong>Original IP</strong>: Leave as &ldquo;Any&rdquo; to allow access from any external IP or specify a range/IP if you have a static IP where you&rsquo;ll be connecting from.</p>
</li>
<li>
<p><strong>Original Port</strong>: Set to the PostgreSQL default port, &ldquo;5432&rdquo;.</p>
</li>
<li>
<p><strong>Forward IP</strong>: Enter the IP address of the machine where PostgreSQL is running, in this case, the VM&rsquo;s IP, <code>10.0.0.2</code>.</p>
</li>
<li>
<p><strong>Forward Port</strong>: Again, set this to &ldquo;5432&rdquo;.</p>
</li>
<li>
<p><strong>Protocol</strong>: PostgreSQL typically uses TCP, so set this to &ldquo;TCP&rdquo;. If there are any reasons to believe you need both TCP and UDP, you can set it to &ldquo;Both&rdquo;, but this is usually not necessary for PostgreSQL.</p>
</li>
</ul>
<h3 id="proxmox-vm-configuration">Proxmox VM Configuration</h3>
<h4 id="1-vm-creation-or-modification">1. VM Creation or Modification</h4>
<p>Now lest assign our newly created VLAN to a VM. Either initiate a new VM or select an existing one.</p>
<ul>
<li>Access the Proxmox web interface.</li>
<li>During the setup or via the &ldquo;Network&rdquo; menu for an existing VM:
<ul>
<li>Set the <strong>Bridge</strong>, you can use your default, typically <code>vmbr0</code>.</li>
<li>Assign the <strong>VLAN Tag</strong> to &ldquo;10&rdquo;.</li>
<li>Ensure the firewall is activated.</li>
<li><strong>IPv4/CIDR</strong>: <code>10.0.0.2/32</code></li>
<li><strong>Gateway</strong>: <code>10.0.0.1</code></li>
</ul>
</li>
<li>Once the VM is started or rebooted, it should automatically acquire the assigned static IP.</li>
</ul>
<h3 id="testing-the-configuration">Testing the Configuration</h3>
<h4 id="1-verify-the-ip-address">1. Verify the IP Address</h4>
<ul>
<li>In Proxmox, access the VM&rsquo;s console.</li>
<li>Execute the <code>ifconfig</code> command to ensure that the IP address <code>10.0.0.2</code> has been correctly assigned.</li>
</ul>
<h4 id="2-test-connectivity">2. Test Connectivity</h4>
<ul>
<li>In the same console, check internet access by pinging an external website: <code>ping www.daangeijs.nl</code>.</li>
<li>Subsequently, attempt to ping a device from your private network. This ping should fail, verifying that the VM is isolated from the private network.</li>
</ul>
<p>There you go! You&rsquo;ve successfully set up a VLAN and isolated a VM within it. You can now host services on this VM and access them from the internet, while keeping your private network secure.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Dockerized Backup and Restore System for Home Directory</title>
      <link>https://www.daangeijs.nl/posts/dockerized-backup/</link>
      <pubDate>Thu, 25 May 2023 12:48:00 +0100</pubDate>
      
      <guid>https://www.daangeijs.nl/posts/dockerized-backup/</guid>
      <description>In this article, I&amp;rsquo;ll guide you through creating a Docker-based system for backing up and restoring your &lt;code&gt;/home/user&lt;/code&gt; directory, storing backups on a mounted HDD, and implementing a 30-day data retention policy. Later, we will discuss how to automate this backup process using &lt;code&gt;crontab&lt;/code&gt;.</description>
      <content:encoded><![CDATA[<h3 id="introduction">Introduction</h3>
<p>In this article, I&rsquo;ll guide you through creating a Docker-based system for backing up and restoring your <code>/home/user</code> directory, storing backups on a mounted HDD, and implementing a 30-day data retention policy. Later, we will discuss how to automate this backup process using <code>crontab</code>.</p>
<h4 id="1-set-up-the-docker-environment">1. Set Up the Docker Environment:</h4>
<h5 id="dockerfile">Dockerfile:</h5>
<p>We&rsquo;ll start with a Docker image based on the Debian slim version, equipped with essential tools for our operations:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-Dockerfile" data-lang="Dockerfile"><span class="line"><span class="cl"><span class="k">FROM</span><span class="s"> debian:bullseye-slim</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">RUN</span> apt-get update <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    apt-get install -y tar <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    rm -rf /var/lib/apt/lists/*<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">WORKDIR</span><span class="s"> /backup</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">COPY</span> .. .<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">RUN</span> chmod +x backup_restore.sh<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">ENTRYPOINT</span> <span class="p">[</span><span class="s2">&#34;./backup_restore.sh&#34;</span><span class="p">]</span><span class="err">
</span></span></span></code></pre></div><p>Here, we&rsquo;re setting up a Debian environment, installing the <code>tar</code> tool for compression, setting a working directory, and copying our backup script.</p>
<h4 id="2-backup-and-restore-script">2. Backup and Restore Script:</h4>
<p>The core logic resides in our <code>backup_restore.sh</code> script, where we define the backup, restore, and cleanup functions.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="c1"># Destination directory on the mounted HDD</span>
</span></span><span class="line"><span class="cl"><span class="nv">BACKUP_DEST</span><span class="o">=</span><span class="s2">&#34;/mnt/storage/backup&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">BACKUP_TARGET_PATH</span><span class="o">=</span><span class="s2">&#34;/home/daan&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">function</span> backup<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    tar czf <span class="si">${</span><span class="nv">BACKUP_DEST</span><span class="si">}</span>/homeserver_backup_<span class="k">$(</span>date +%Y%m%d<span class="k">)</span>.tar.gz -C <span class="si">${</span><span class="nv">BACKUP_TARGET_PATH</span><span class="si">}</span> .
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">function</span> restore<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">tarball</span><span class="o">=</span><span class="nv">$1</span>
</span></span><span class="line"><span class="cl">    <span class="nb">local</span> <span class="nv">restore_path</span><span class="o">=</span><span class="nv">$2</span>
</span></span><span class="line"><span class="cl">    tar xzf <span class="si">${</span><span class="nv">BACKUP_DEST</span><span class="si">}</span>/<span class="si">${</span><span class="nv">tarball</span><span class="si">}</span> -C <span class="si">${</span><span class="nv">restore_path</span><span class="si">}</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">function</span> cleanup<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">    find <span class="si">${</span><span class="nv">BACKUP_DEST</span><span class="si">}</span> -name <span class="s1">&#39;homeserver_backup_*.tar.gz&#39;</span> -mtime +30 -exec rm <span class="o">{}</span> <span class="se">\;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;backup&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    backup
</span></span><span class="line"><span class="cl">    cleanup
</span></span><span class="line"><span class="cl"><span class="k">elif</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;restore&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> -n <span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> -n <span class="s2">&#34;</span><span class="nv">$3</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    restore <span class="nv">$2</span> <span class="nv">$3</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Usage:&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;./backup_restore.sh backup&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;./backup_restore.sh restore tarball_filename /path/to/restore&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span></code></pre></div><p><strong>Functions Explained</strong>:</p>
<ul>
<li>
<p><strong>backup()</strong>: This function compresses and archives the <code>/home/user</code> directory into a tarball, which is saved directly to the mounted HDD.</p>
</li>
<li>
<p><strong>restore()</strong>: Allows the user to specify a backup tarball and restore its contents to a given directory.</p>
</li>
<li>
<p><strong>cleanup()</strong>: Implements the data retention policy by deleting backups older than 30 days from the mounted HDD.</p>
</li>
</ul>
<h4 id="3-building-and-running-the-docker-container">3. Building and Running the Docker Container:</h4>
<p>First, build your Docker image:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker build -t userbackuprestore:latest .
</span></span></code></pre></div><p>To run the Docker container:</p>
<ul>
<li><strong>For backup</strong>:</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm -v /home/user:/home/user -v /mnt/storage:/mnt/storage userbackuprestore:latest backup
</span></span></code></pre></div><ul>
<li><strong>For restore</strong>:</li>
</ul>
<p>Replace <code>your_tarball_filename.tar.gz</code> with the desired backup tarball&rsquo;s filename and <code>/path/to/restore</code> with the directory where you want to restore.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm -v /path/to/restore:/restore -v /mnt/storage:/mnt/storage userbackuprestore:latest restore your_tarball_filename.tar.gz /restore
</span></span></code></pre></div><h3 id="automating-backups-using-crontab">Automating Backups Using Crontab:</h3>
<p>After setting up the Docker-based backup system, automating the process ensures that backups are taken regularly without manual intervention. The <code>cron</code> job scheduler is an excellent tool for this. Here’s how you can schedule the backup task using <code>crontab</code>.</p>
<h4 id="1-open-crontab">1. Open Crontab:</h4>
<p>To edit the current user&rsquo;s <code>crontab</code> entries, use:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">crontab -e
</span></span></code></pre></div><p>This will open up the default editor, often <code>vi</code> or <code>nano</code>, depending on the system setup.</p>
<h4 id="2-add-a-cron-job">2. Add a Cron Job:</h4>
<p>To run the backup daily at, say, 2:30 AM, add the following line:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="m">30</span> <span class="m">2</span> * * * docker run --rm -v /home/user:/home/user -v /mnt/storage:/mnt/storage userbackuprestore:latest backup
</span></span></code></pre></div><p>The general format of a cron job is:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="o">[</span>min<span class="o">]</span> <span class="o">[</span>hour<span class="o">]</span> <span class="o">[</span>day of month<span class="o">]</span> <span class="o">[</span>month<span class="o">]</span> <span class="o">[</span>day of week<span class="o">]</span> <span class="o">[</span>command<span class="o">]</span>
</span></span></code></pre></div><p>For this job:</p>
<ul>
<li><code>30</code> is the minute (30 minutes past the hour).</li>
<li><code>2</code> is the hour (2 AM).</li>
<li><code>*</code> for day of month, month, and day of week indicates &ldquo;every&rdquo; or &ldquo;any.&rdquo;</li>
</ul>
<p>Therefore, <code>30 2 * * *</code> means &ldquo;2:30 AM, every day.&rdquo;</p>
<h4 id="3-save-and-exit">3. Save and Exit:</h4>
<p>After adding the line:</p>
<ul>
<li>If you&rsquo;re in <code>vi</code>, press <code>Esc</code>, type <code>:wq</code>, and press <code>Enter</code>.</li>
<li>If you&rsquo;re in <code>nano</code>, press <code>CTRL + X</code>, press <code>Y</code> to confirm changes, and press <code>Enter</code> to save.</li>
</ul>
<h4 id="4-verify-the-cron-job">4. Verify the Cron Job:</h4>
<p>To ensure your cron job has been set correctly, you can display the current user&rsquo;s <code>crontab</code> entries:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">crontab -l
</span></span></code></pre></div><p>You should see the line you added for the backup</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Proxmox: how to Expand Storage in a Linux VM with LVM</title>
      <link>https://www.daangeijs.nl/posts/proxmox-storage/</link>
      <pubDate>Mon, 15 May 2023 12:48:00 +0100</pubDate>
      
      <guid>https://www.daangeijs.nl/posts/proxmox-storage/</guid>
      <description>After bolstering the storage capacity of a Linux VM within Proxmox or another hypervisor, you&amp;rsquo;ll discover that the operating system inside the VM doesn&amp;rsquo;t automatically recognize or utilize this newly added space.</description>
      <content:encoded><![CDATA[<h4 id="introduction">Introduction</h4>
<p><strong>Problem</strong>:
After bolstering the storage capacity of a Linux VM within Proxmox or another hypervisor, you&rsquo;ll discover that the operating system inside the VM doesn&rsquo;t automatically recognize or utilize this newly added space.</p>
<p><strong>Solution</strong>:
To harness this additional storage, you need to resize both the partitions and the filesystems within the Linux environment. If you&rsquo;ve configured your VM with Logical Volume Management (LVM), this entails adjusting the physical volume, the logical volume, and subsequently, the filesystem.</p>
<ol>
<li>
<p><strong>Ensure the Partition Covers the New Space</strong>:</p>
<p>To observe the current partitions:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo fdisk -l
</span></span></code></pre></div><p><strong>Example Output</strong>:</p>
<pre tabindex="0"><code>Device     Start      End  Sectors  Size Type
/dev/sda1   2048   999423   997376  487M EFI System
/dev/sda2 999424 20479999 19480576  9.3G Linux filesystem
</code></pre><p>If, for instance, <code>/dev/sda2</code> isn&rsquo;t leveraging the entire disk space, you need to adjust it. Here&rsquo;s how:</p>
<p>Launch the partition tool for the disk:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo fdisk /dev/sda
</span></span></code></pre></div><ul>
<li>Press <code>p</code> to display the existing partition layout.</li>
<li>Ensure you note the start sector of the partition you intend to resize.</li>
<li>Press <code>d</code> to delete the desired partition, and select its number (like <code>2</code> for <code>/dev/sda2</code>).</li>
<li>Hit <code>n</code> to create a new partition. Use the exact start sector from earlier and allow the default end sector to encompass all available space.</li>
<li>Press <code>t</code> to modify the partition type and assign it to <code>8e</code>, denoting Linux LVM.</li>
<li>To apply the changes, press <code>w</code>.</li>
</ul>
<p><strong>Example Output</strong>:</p>
<pre tabindex="0"><code>Command (m for help): n
Partition number (2-4, default 2): 2
First sector (999424-49971199, default 999424): 999424
Last sector, +/-sectors or +/-size{K,M,G,T,P} (999424-49971199, default 49971199): 
Created a new partition 2 of type &#39;Linux filesystem&#39; and of size 23.3 GiB.
</code></pre></li>
<li>
<p><strong>Inform the Kernel About Partition Changes</strong>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo partprobe
</span></span></code></pre></div></li>
<li>
<p><strong>Expand the LVM Physical Volume</strong>:</p>
<p>To view the current status:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pvdisplay
</span></span></code></pre></div><p><strong>Example Output</strong>:</p>
<pre tabindex="0"><code>--- Physical volume ---
PV Name               /dev/sda2
VG Name               ubuntu-vg
PV Size               9.30 GiB
</code></pre><p>To resize the physical volume:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo pvresize /dev/sda2
</span></span></code></pre></div></li>
<li>
<p><strong>Expand the LVM Logical Volume</strong>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo lvresize -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
</span></span></code></pre></div></li>
<li>
<p><strong>Resize the Filesystem</strong>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
</span></span></code></pre></div></li>
<li>
<p><strong>Verify the Changes</strong>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">df -h
</span></span></code></pre></div><p><strong>Example Output</strong>:</p>
<pre tabindex="0"><code>Filesystem                             Size  Used Avail Use% Mounted on
/dev/mapper/ubuntu--vg-ubuntu--lv      23G  4.8G   17G  22% /
</code></pre></li>
</ol>
<h4 id="lvm-vs-logical-volume-whats-the-difference">LVM vs. Logical Volume: What&rsquo;s the Difference?</h4>
<p>LVM (Logical Volume Management) offers a flexible and agile solution for storage management, enabling the seamless management of disk drives and similar storage mechanisms. Under the LVM umbrella, you&rsquo;ll encounter:</p>
<ul>
<li><strong>Physical Volumes (PV)</strong>: These are your raw storage devices or partitions that store data.</li>
<li><strong>Volume Groups (VG)</strong>: Collections of physical volumes, they act as one consolidated storage reservoir.</li>
<li><strong>Logical Volumes (LV)</strong>: These lie within a volume group and act as block devices that sustain the filesystem.</li>
</ul>
<p>In essence, LVM facilitates the amalgamation of several disks (or partitions) into a singular storage pool (VG). From this pool, logical subdivisions (LVs) can be extracted, upon which filesystems are created.</p>
<p>Thus, when we discuss &ldquo;expanding LVM&rdquo;, we&rsquo;re essentially alluding to a series of tasks that incorporate expanding the physical volume (PV), possibly the volume group (VG), followed by the logical volume (LV). Only post these operations is the filesystem itself stretched to occupy the new space.</p>
<hr>
<p>Endowed with these precise commands and sample outputs, you&rsquo;re equipped with a clear roadmap of what to anticipate at every juncture. As a precaution, always ensure your data is backed up before initiating any significant modifications to your storage structures.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Automating MariaDB Backups in Docker with a Shell Script</title>
      <link>https://www.daangeijs.nl/posts/mariadb-backup/</link>
      <pubDate>Tue, 25 Apr 2023 12:48:00 +0100</pubDate>
      
      <guid>https://www.daangeijs.nl/posts/mariadb-backup/</guid>
      <description>Backing up your data regularly is essential, especially for critical applications like Home Assistant running MariaDB in a Docker container. This article will guide you through creating scripts that automate the backup and restoration processes, designed to be flexible by accepting arguments for the password and paths.</description>
      <content:encoded><![CDATA[<h3 id="introduction">Introduction</h3>
<p>Backing up your data regularly is essential, especially for critical applications like Home Assistant running MariaDB in a Docker container. This article will guide you through creating scripts that automate the backup and restoration processes, designed to be flexible by accepting arguments for the password and paths.</p>
<h3 id="prerequisites">Prerequisites</h3>
<ul>
<li>Docker installed and running.</li>
<li>MariaDB container running with your data.</li>
</ul>
<h3 id="backup-script-with-arguments">Backup Script with Arguments</h3>
<p>To make our backup process versatile and reusable, we&rsquo;ll create a shell script that accepts two arguments:</p>
<ol>
<li>MariaDB password</li>
<li>Backup folder path</li>
</ol>
<h4 id="script-creation">Script Creation</h4>
<ol>
<li>Create and open a new script:</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">nano /path_to_scripts_folder/backup_mariadb.sh
</span></span></code></pre></div><ol start="2">
<li>Copy and paste the following content:</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="c1"># Check if the right number of arguments are provided</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$#</span><span class="s2">&#34;</span> -ne <span class="m">2</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Usage: </span><span class="nv">$0</span><span class="s2"> &lt;password&gt; &lt;backup_folder_path&gt;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">PASSWORD</span><span class="o">=</span><span class="nv">$1</span>
</span></span><span class="line"><span class="cl"><span class="nv">BACKUP_PATH</span><span class="o">=</span><span class="nv">$2</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">docker <span class="nb">exec</span> mariadb /usr/bin/mysqldump -u homeassistant --password<span class="o">=</span><span class="nv">$PASSWORD</span> --all-databases <span class="p">|</span> gzip &gt; <span class="s2">&#34;</span><span class="nv">$BACKUP_PATH</span><span class="s2">/database_backup_</span><span class="k">$(</span>date +<span class="se">\%</span>F<span class="k">)</span><span class="s2">.sql.gz&#34;</span>
</span></span></code></pre></div><ol start="3">
<li>
<p>Save and exit the editor.</p>
</li>
<li>
<p>Make the script executable:</p>
</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">chmod +x /path_to_scripts_folder/backup_mariadb.sh
</span></span></code></pre></div><p>Now, run the script, passing the password and backup folder path as arguments:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">/path_to_scripts_folder/backup_mariadb.sh your_password /path_to_backup_folder/
</span></span></code></pre></div><h3 id="scheduling-backups-with-crontab">Scheduling Backups with Crontab</h3>
<p>To automate the backup process daily:</p>
<ol>
<li>Open the crontab:</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">crontab -e
</span></span></code></pre></div><ol start="2">
<li>Add the following line to run the script every day at 3 am:</li>
</ol>
<pre tabindex="0"><code>0 3 * * * /path_to_scripts_folder/backup_mariadb.sh your_password /path_to_backup_folder/
</code></pre><h3 id="restoring-the-database-from-backup">Restoring the Database from Backup</h3>
<h4 id="restoration-script-with-arguments">Restoration Script with Arguments</h4>
<p>To simplify the restoration process, we&rsquo;ll create a separate shell script that accepts two arguments:</p>
<ol>
<li>MariaDB password</li>
<li>Path to the backup file you want to restore</li>
</ol>
<h5 id="script-creation-1">Script Creation</h5>
<ol>
<li>Create and open a new script:</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">nano /path_to_scripts_folder/restore_mariadb.sh
</span></span></code></pre></div><ol start="2">
<li>Copy and paste the following content:</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="c1"># Check if the right number of arguments are provided</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$#</span><span class="s2">&#34;</span> -ne <span class="m">2</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Usage: </span><span class="nv">$0</span><span class="s2"> &lt;password&gt; &lt;backup_file_path&gt;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">PASSWORD</span><span class="o">=</span><span class="nv">$1</span>
</span></span><span class="line"><span class="cl"><span class="nv">BACKUP_FILE</span><span class="o">=</span><span class="nv">$2</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">gunzip &lt; <span class="s2">&#34;</span><span class="nv">$BACKUP_FILE</span><span class="s2">&#34;</span> <span class="p">|</span> docker <span class="nb">exec</span> -i mariadb /usr/bin/mysql -u homeassistant --password<span class="o">=</span><span class="nv">$PASSWORD</span>
</span></span></code></pre></div><ol start="3">
<li>
<p>Save and exit the editor.</p>
</li>
<li>
<p>Make the script executable:</p>
</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">chmod +x /path_to_scripts_folder/restore_mariadb.sh
</span></span></code></pre></div><p>Now, run the script, passing the password and the path to the backup file as arguments:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">/path_to_scripts_folder/restore_mariadb.sh your_password /path_to_backup_folder/database_backup_YOUR_DATE.sql.gz
</span></span></code></pre></div><h4 id="notes-on-restoration">Notes on Restoration</h4>
<ol>
<li><strong>Backup before restore</strong>: Always take a fresh backup before starting the restoration process. This ensures you have a fallback if the restore doesn&rsquo;t go as planned.</li>
<li><strong>Check Compatibility</strong>: Ensure the MariaDB version you are restoring to is compatible with the version from which you took the backup.</li>
<li><strong>Downtime Considerations</strong>: Depending on the size of your database and the restoration environment&rsquo;s performance, the restoration process might take some time. Plan accordingly.</li>
</ol>
]]></content:encoded>
    </item>
    
    <item>
      <title>Exporting a .bib File from Google Scholar and Generating TOML-formatted Markdown Files</title>
      <link>https://www.daangeijs.nl/posts/bib-to-markdown/</link>
      <pubDate>Sat, 25 Mar 2023 12:48:00 +0100</pubDate>
      
      <guid>https://www.daangeijs.nl/posts/bib-to-markdown/</guid>
      <description>This post will show you how I added the publications on my website, that is generated by Hugo. I will try to provide step-by-step instructions on exporting your .bib file from Google Scholar and converting it into TOML-formatted markdown files using Python.</description>
      <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>This post will show you how I added the publications on my website, that is generated by Hugo. I will try to provide step-by-step instructions on exporting your .bib file from Google Scholar and converting it into TOML-formatted markdown files using Python.</p>
<h2 id="step-1-exporting-bib-from-google-scholar">Step 1: Exporting .bib from Google Scholar</h2>
<ol>
<li><strong>Login to Google Scholar</strong>: Head over to <a href="https://scholar.google.com/">Google Scholar</a> and sign in using your credentials.</li>
<li><strong>Access &ldquo;My Library&rdquo;</strong>: Here you will find all your saved articles and citations.</li>
<li><strong>Select Articles</strong>: Choose the articles you wish to export by ticking the checkboxes next to them.</li>
<li><strong>Click the Export Button</strong>: This button, usually represented by quotation marks, will give you different export options.</li>
<li><strong>Choose BibTeX</strong>: Select the BibTeX option to export your articles in the .bib format.</li>
<li><strong>Save the File</strong>: The BibTeX formatted content will be displayed in a new window. Copy this content and save it in a <code>.bib</code> file, for example, <code>references.bib</code>.</li>
</ol>
<h2 id="step-2-converting-bib-to-toml-formatted-markdown-with-python">Step 2: Converting .bib to TOML-formatted Markdown with Python</h2>
<p>Now that we have our .bib file, let&rsquo;s use Python to parse the file and create individual markdown files with TOML front matter.</p>
<h3 id="a-setting-up-the-environment">a. Setting Up the Environment</h3>
<p>Before delving into the code, make sure you have the required Python libraries:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install pybtex pylatexenc
</span></span></code></pre></div><h3 id="b-code-breakdown">b. Code Breakdown</h3>
<h4 id="i-key-imports">i. Key Imports:</h4>
<ul>
<li><strong>os &amp; pathlib.Path</strong>: Used for directory and file path operations.</li>
<li><strong>bibtex</strong>: To parse the .bib file.</li>
<li><strong>latex2text</strong>: From <code>pylatexenc</code>, used to convert LaTeX to Unicode.</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pybtex.database.input</span> <span class="kn">import</span> <span class="n">bibtex</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pylatexenc.latex2text</span> <span class="kn">import</span> <span class="n">LatexNodes2Text</span>
</span></span></code></pre></div><h4 id="ii-latex-to-unicode-conversion">ii. LaTeX to Unicode Conversion:</h4>
<p>We&rsquo;ll use the <code>pylatexenc</code> library to easily convert LaTeX-specific text into Unicode. This is especially useful for author names, titles, or sources that might use LaTeX-style formatting.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">latex_to_unicode</span><span class="p">(</span><span class="n">text</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">LatexNodes2Text</span><span class="p">()</span><span class="o">.</span><span class="n">latex_to_text</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
</span></span></code></pre></div><h4 id="iii-generating-markdown">iii. Generating Markdown:</h4>
<p>The <code>create_or_update_md</code> function will be responsible for converting each .bib entry into its markdown equivalent with TOML front matter. By default it will set the publication to <code>hidden=true</code>. When hidden the publication will not be listed as an article, but will be listed as non-clickable entry in the partial.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">create_or_update_md</span><span class="p">(</span><span class="n">entry</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Extracting common fields</span>
</span></span><span class="line"><span class="cl">    <span class="n">key</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">key</span>
</span></span><span class="line"><span class="cl">    <span class="n">title</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;title&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">year</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;year&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">journal</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;journal&#39;</span><span class="p">,</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;booktitle&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">volume</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;volume&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">pages</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;pages&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">publisher</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;publisher&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;url&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Extracting the author list and convert it to a list of authors</span>
</span></span><span class="line"><span class="cl">    <span class="n">authors</span> <span class="o">=</span> <span class="p">[</span><span class="n">latex_to_unicode</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">author</span><span class="p">))</span> <span class="k">for</span> <span class="n">author</span> <span class="ow">in</span> <span class="n">entry</span><span class="o">.</span><span class="n">persons</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;author&#39;</span><span class="p">,</span> <span class="p">[])]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Define the path</span>
</span></span><span class="line"><span class="cl">    <span class="n">folder_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;content/publications/</span><span class="si">{</span><span class="n">key</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">file_path</span> <span class="o">=</span> <span class="n">folder_path</span> <span class="o">/</span> <span class="s2">&#34;index.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Check if file exists</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">file_path</span><span class="o">.</span><span class="n">exists</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># If file doesn&#39;t exist, create a new markdown file with TOML front matter</span>
</span></span><span class="line"><span class="cl">        <span class="n">folder_path</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">parents</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="s1">&#39;w&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">&#34;+++</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;title = &#34;</span><span class="si">{</span><span class="n">title</span><span class="si">}</span><span class="s1">&#34;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s1">&#39;hidden = true</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;authors  = </span><span class="si">{</span><span class="n">authors</span><span class="si">}</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;date = </span><span class="si">{</span><span class="n">year</span><span class="si">}</span><span class="s1">-01-01</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;journal = &#34;</span><span class="si">{</span><span class="n">journal</span><span class="si">}</span><span class="s1">&#34;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;volume = &#34;</span><span class="si">{</span><span class="n">volume</span><span class="si">}</span><span class="s1">&#34;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;pages = &#34;</span><span class="si">{</span><span class="n">pages</span><span class="si">}</span><span class="s1">&#34;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;publisher = &#34;</span><span class="si">{</span><span class="n">publisher</span><span class="si">}</span><span class="s1">&#34;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;url = &#34;</span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="s1">&#34;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">&#34;+++</span><span class="se">\n\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Summary about </span><span class="si">{</span><span class="n">title</span><span class="si">}</span><span class="s2">.&#34;</span><span class="p">)</span>
</span></span></code></pre></div><h4 id="iv-parsing--execution">iv. Parsing &amp; Execution:</h4>
<p>Here, we parse the .bib file and process each entry:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span> <span class="o">=</span> <span class="n">bibtex</span><span class="o">.</span><span class="n">Parser</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">bib_data</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="n">parse_file</span><span class="p">(</span><span class="s2">&#34;resources/references.bib&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">entry</span> <span class="ow">in</span> <span class="n">bib_data</span><span class="o">.</span><span class="n">entries</span><span class="o">.</span><span class="n">values</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">create_or_update_md</span><span class="p">(</span><span class="n">entry</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">main</span><span class="p">()</span>
</span></span></code></pre></div><h2 id="step-3-displaying-in-hugo">Step 3: Displaying in Hugo</h2>
<p>You will need font-awesome package, you can add this head to your partial or in a included template, but just make sure you include it somewhere.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">link</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;stylesheet&#34;</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>After running the Python script, you&rsquo;ll have a collection of markdown files ready to be displayed in Hugo. Here&rsquo;s how you can showcase them using the provided Hugo partials.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="p">{{</span><span class="o">-</span> <span class="err">$</span><span class="nx">pages</span> <span class="o">:=</span> <span class="nx">where</span> <span class="p">.</span><span class="nx">Site</span><span class="p">.</span><span class="nx">RegularPages</span> <span class="s">&#34;Section&#34;</span> <span class="s">&#34;publications&#34;</span><span class="p">}}</span>
</span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="o">-</span> <span class="err">$</span><span class="nx">sortedPages</span> <span class="o">:=</span> <span class="err">$</span><span class="nx">pages</span><span class="p">.</span><span class="nx">ByDate</span><span class="p">.</span><span class="nx">Reverse</span> <span class="p">}}</span>
</span></span><span class="line"><span class="cl"><span class="p">{{</span><span class="o">-</span> <span class="err">$</span><span class="nx">latestPublications</span> <span class="o">:=</span> <span class="err">$</span><span class="nx">sortedPages</span> <span class="p">|</span> <span class="nx">first</span> <span class="mi">5</span> <span class="p">}}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nx">div</span> <span class="nx">class</span><span class="p">=</span><span class="s">&#34;latest-publications&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nx">h2</span><span class="p">&gt;</span><span class="nx">Latest</span> <span class="nx">Publications</span><span class="p">&lt;</span><span class="o">/</span><span class="nx">h2</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nx">ul</span> <span class="nx">class</span><span class="p">=</span><span class="s">&#34;&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">{{</span><span class="o">-</span> <span class="k">range</span> <span class="err">$</span><span class="nx">latestPublications</span> <span class="p">}}</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="nx">li</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="p">{{</span> <span class="err">$</span><span class="nx">currentPublication</span> <span class="o">:=</span> <span class="p">.</span> <span class="p">}}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">            <span class="p">{{</span> <span class="k">if</span> <span class="nx">ne</span> <span class="p">.</span><span class="nx">Params</span><span class="p">.</span><span class="nx">hidden</span> <span class="kc">true</span> <span class="p">}}</span>
</span></span><span class="line"><span class="cl">            <span class="p">&lt;</span><span class="nx">a</span> <span class="nx">href</span><span class="p">=</span><span class="s">&#34;{{ .Permalink }}&#34;</span><span class="p">&gt;{{</span> <span class="p">.</span><span class="nx">Title</span> <span class="p">}}</span> <span class="p">&lt;</span><span class="nx">i</span> <span class="nx">class</span><span class="p">=</span><span class="s">&#34;fas fa-file-alt&#34;</span><span class="p">&gt;&lt;</span><span class="o">/</span><span class="nx">i</span><span class="p">&gt;&lt;</span><span class="o">/</span><span class="nx">a</span><span class="p">&gt;</span> <span class="p">({{</span> <span class="p">.</span><span class="nx">Date</span><span class="p">.</span><span class="nx">Format</span> <span class="s">&#34;2006&#34;</span> <span class="p">}})</span>
</span></span><span class="line"><span class="cl">            <span class="p">{{</span> <span class="k">else</span> <span class="p">}}</span>
</span></span><span class="line"><span class="cl">            <span class="p">{{</span> <span class="p">.</span><span class="nx">Title</span> <span class="p">}}</span> <span class="p">({{</span> <span class="p">.</span><span class="nx">Date</span><span class="p">.</span><span class="nx">Format</span> <span class="s">&#34;2006&#34;</span> <span class="p">}})</span>
</span></span><span class="line"><span class="cl">            <span class="p">{{</span> <span class="nx">end</span> <span class="p">}}</span>
</span></span><span class="line"><span class="cl">            <span class="p">&lt;</span><span class="nx">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">                <span class="p">{{</span> <span class="k">range</span> <span class="err">$</span><span class="nx">i</span><span class="p">,</span> <span class="err">$</span><span class="nx">author</span> <span class="o">:=</span> <span class="p">.</span><span class="nx">Params</span><span class="p">.</span><span class="nx">authors</span> <span class="p">}}</span>
</span></span><span class="line"><span class="cl">                <span class="p">{{</span> <span class="k">if</span> <span class="nx">lt</span> <span class="err">$</span><span class="nx">i</span> <span class="mi">1</span> <span class="p">}}{{</span> <span class="err">$</span><span class="nx">author</span> <span class="p">}}{{</span> <span class="k">if</span> <span class="nx">lt</span> <span class="err">$</span><span class="nf">i</span> <span class="p">(</span><span class="nf">sub</span> <span class="p">(</span><span class="nx">len</span> <span class="err">$</span><span class="nx">currentPublication</span><span class="p">.</span><span class="nx">Params</span><span class="p">.</span><span class="nx">authors</span><span class="p">)</span> <span class="mi">1</span><span class="p">)</span> <span class="p">}}</span> <span class="nx">and</span> <span class="p">{{</span> <span class="nx">end</span>
</span></span><span class="line"><span class="cl">                <span class="p">}}{{</span> <span class="nx">end</span> <span class="p">}}</span>
</span></span><span class="line"><span class="cl">                <span class="p">{{</span> <span class="k">if</span> <span class="nx">eq</span> <span class="err">$</span><span class="nx">i</span> <span class="mi">1</span> <span class="p">}}{{</span> <span class="err">$</span><span class="nx">author</span> <span class="p">}}</span> <span class="nx">et</span> <span class="nx">al</span><span class="p">.{{</span> <span class="nx">end</span> <span class="p">}}</span>
</span></span><span class="line"><span class="cl">                <span class="p">{{</span> <span class="nx">end</span> <span class="p">}}</span>
</span></span><span class="line"><span class="cl">            <span class="p">&lt;</span><span class="o">/</span><span class="nx">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="o">/</span><span class="nx">li</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">{{</span><span class="o">-</span> <span class="nx">end</span> <span class="p">}}</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="o">/</span><span class="nx">ul</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="o">/</span><span class="nx">div</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>Simply embed this partial into your desired Hugo template, and your publications will be presented.</p>
<h2 id="the-complete-python-script">The Complete Python Script:</h2>
<p>For those who want to dive straight in, here&rsquo;s the full Python script:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pybtex.database.input</span> <span class="kn">import</span> <span class="n">bibtex</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pylatexenc.latex2text</span> <span class="kn">import</span> <span class="n">LatexNodes2Text</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">re</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">latex_to_unicode</span><span class="p">(</span><span class="n">text</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">LatexNodes2Text</span><span class="p">()</span><span class="o">.</span><span class="n">latex_to_text</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">create_or_update_md</span><span class="p">(</span><span class="n">entry</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Extracting common fields</span>
</span></span><span class="line"><span class="cl">    <span class="n">key</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">key</span>
</span></span><span class="line"><span class="cl">    <span class="n">title</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;title&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">year</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;year&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">journal</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;journal&#39;</span><span class="p">,</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;booktitle&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">volume</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;volume&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">pages</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;pages&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">publisher</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;publisher&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">url</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">fields</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;url&#39;</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Extracting the author list and convert it to a list of authors</span>
</span></span><span class="line"><span class="cl">    <span class="n">authors</span> <span class="o">=</span> <span class="p">[</span><span class="n">latex_to_unicode</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">author</span><span class="p">))</span> <span class="k">for</span> <span class="n">author</span> <span class="ow">in</span> <span class="n">entry</span><span class="o">.</span><span class="n">persons</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;author&#39;</span><span class="p">,</span> <span class="p">[])]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Define the path</span>
</span></span><span class="line"><span class="cl">    <span class="n">folder_path</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;content/publications/</span><span class="si">{</span><span class="n">key</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">file_path</span> <span class="o">=</span> <span class="n">folder_path</span> <span class="o">/</span> <span class="s2">&#34;index.md&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Check if file exists</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">file_path</span><span class="o">.</span><span class="n">exists</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># If file doesn&#39;t exist, create a new markdown file with TOML front matter</span>
</span></span><span class="line"><span class="cl">        <span class="n">folder_path</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">parents</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span> <span class="s1">&#39;w&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">&#34;+++</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;title = &#34;</span><span class="si">{</span><span class="n">title</span><span class="si">}</span><span class="s1">&#34;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s1">&#39;hidden = true</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;authors  = </span><span class="si">{</span><span class="n">authors</span><span class="si">}</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;date = </span><span class="si">{</span><span class="n">year</span><span class="si">}</span><span class="s1">-01-01</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;journal = &#34;</span><span class="si">{</span><span class="n">journal</span><span class="si">}</span><span class="s1">&#34;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;volume = &#34;</span><span class="si">{</span><span class="n">volume</span><span class="si">}</span><span class="s1">&#34;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;pages = &#34;</span><span class="si">{</span><span class="n">pages</span><span class="si">}</span><span class="s1">&#34;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;publisher = &#34;</span><span class="si">{</span><span class="n">publisher</span><span class="si">}</span><span class="s1">&#34;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;url = &#34;</span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="s1">&#34;</span><span class="se">\n</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="s2">&#34;+++</span><span class="se">\n\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Summary about </span><span class="si">{</span><span class="n">title</span><span class="si">}</span><span class="s2">.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_author_list</span><span class="p">(</span><span class="n">entry</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="s1">&#39;author&#39;</span> <span class="ow">in</span> <span class="n">entry</span><span class="o">.</span><span class="n">persons</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">authors</span> <span class="o">=</span> <span class="n">entry</span><span class="o">.</span><span class="n">persons</span><span class="p">[</span><span class="s1">&#39;author&#39;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">        <span class="n">author_str</span> <span class="o">=</span> <span class="s1">&#39; and &#39;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="nb">str</span><span class="p">(</span><span class="n">author</span><span class="p">)</span> <span class="k">for</span> <span class="n">author</span> <span class="ow">in</span> <span class="n">authors</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">latex_to_unicode</span><span class="p">(</span><span class="n">author_str</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s1">&#39;&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">update_field</span><span class="p">(</span><span class="n">content</span><span class="p">,</span> <span class="n">field</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># If the field exists, update it. If not, just return the content as is.</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">field</span><span class="si">}</span><span class="s2">:&#34;</span> <span class="ow">in</span> <span class="n">content</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">content</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;</span><span class="si">{</span><span class="n">field</span><span class="si">}</span><span class="s1">:.*&#39;</span><span class="p">,</span> <span class="sa">f</span><span class="s1">&#39;</span><span class="si">{</span><span class="n">field</span><span class="si">}</span><span class="s1">: &#34;</span><span class="si">{</span><span class="n">value</span><span class="si">}</span><span class="s1">&#34;&#39;</span><span class="p">,</span> <span class="n">content</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">content</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">parser</span> <span class="o">=</span> <span class="n">bibtex</span><span class="o">.</span><span class="n">Parser</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">bib_data</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="n">parse_file</span><span class="p">(</span><span class="s2">&#34;resources/references.bib&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">entry</span> <span class="ow">in</span> <span class="n">bib_data</span><span class="o">.</span><span class="n">entries</span><span class="o">.</span><span class="n">values</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">create_or_update_md</span><span class="p">(</span><span class="n">entry</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">main</span><span class="p">()</span>
</span></span></code></pre></div>]]></content:encoded>
    </item>
    
  </channel>
</rss>
