<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Jerry Ng's blog]]></title><description><![CDATA[Jerry Ng's blog]]></description><link>https://jerrynsh.com/</link><image><url>https://jerrynsh.com/favicon.png</url><title>Jerry Ng&apos;s blog</title><link>https://jerrynsh.com/</link></image><generator>Ghost 5.76</generator><lastBuildDate>Fri, 23 Feb 2024 01:35:40 GMT</lastBuildDate><atom:link href="https://jerrynsh.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[How to Manage and Update Python Version]]></title><description><![CDATA[Effortlessly switch Python version with a reliable Python version manager. Update your Python version and other CLI tools with just asdf!]]></description><link>https://jerrynsh.com/how-to-manage-and-update-python-version/</link><guid isPermaLink="false">65bc4d14037fbb99a0004129</guid><category><![CDATA[Python]]></category><dc:creator><![CDATA[Jerry Ng]]></dc:creator><pubDate>Mon, 05 Feb 2024 00:00:26 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1502570149819-b2260483d302?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fG51bWJlcnN8ZW58MHx8fHwxNzA2ODM5MzQwfDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1502570149819-b2260483d302?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDJ8fG51bWJlcnN8ZW58MHx8fHwxNzA2ODM5MzQwfDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="How to Manage and Update Python Version"><p>We&#x2019;ve all been there &#x2014; there comes a time when we must update our Python version to meet a different Python version yet, be it at work or when working on personal projects.</p><p>Just a quick search about &#x201C;update python version&#x201D; and we will be bombarded with suggestions to run <code>python --version</code> followed by <code>brew upgrade python3</code> or <code>sudo apt-get update</code>.</p><p>Okay cool. Problem solved right?</p><p>Not really. Probably 9/10 times an upgrade won&#x2019;t cut it &#x2014; enter another project. And guess what? It wants a different Python version, maybe an older one just to make our life a little bit more miserable.</p><p>So, here we are, stuck between a rock and a hard place, asking ourselves, &quot;<em>Do I downgrade Python now? But what if I need to juggle both projects? I didn&apos;t sign up for this </em><a href="https://stackoverflow.com/questions/69470556/python-symlink-to-python3"><em>symlink</em></a><em> or </em><a href="https://realpython.com/add-python-to-path/"><em>PATH variable</em></a><em> wrestling match!</em>&#x201D;</p><h2 id="lets-use-pyenv">Let&apos;s use <code>pyenv</code>?</h2><p>We know it&#x2019;s not uncommon either to find ourselves in another project needing a different Python version yet.</p><p>Now, with some quick search, you can tell most Python folks swear by <code>pyenv</code>  (<a href="https://github.com/pyenv/pyenv">docs</a>) for managing Python versions.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Tip on a quick search: <a href="https://jerrynsh.com/how-to-google-with-a-bang/">How to Google With a Bang!</a></div></div><p>Don&#x2019;t get me wrong, it works but it&#x2019;s just not for me.</p><p>Well, given that I work with a lot of CLI tools like <code>go</code>, <code>node</code>, <code>terraform</code>, <code>git</code>, etc., I prefer the simplicity of using <strong>a single tool  &#x2013; </strong><a href="https://github.com/asdf-vm/asdf"><code>asdf</code></a>.</p><p>In other words, I very much prefer to use <code>asdf</code> to manage all my programming language or CLI tool versions, rather than dealing with the likes of <a href="https://github.com/moovweb/gvm"><code>gvm</code></a>, <a href="https://github.com/nvm-sh/nvm"><code>nvm</code></a>, and <a href="https://github.com/pyenv/pyenv"><code>pyenv</code></a> separately.</p><h2 id="asdf-python-quick-guide"><code>asdf</code> Python Quick Guide</h2><p>Beyond Python, <code>asdf</code> supports various plugins. But, let&apos;s focus on Python without delving into exhaustive details.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Hint: run <code spellcheck="false" style="white-space: pre-wrap;">asdf plugin list all</code> to list all available plugins.</div></div><h3 id="installation">Installation</h3><p>Easy, just follow <a href="https://asdf-vm.com/guide/getting-started.html">asdf-vm.com/guide/getting-started.html</a> based on your system specifications:</p><ol><li>OS (e.g. linux, macOS)</li><li>Package manager (e.g. <code>brew</code>, <code>apt</code>, <code>pacman</code>, etc.)</li><li>Shell (e.g. <code>zsh</code>, <code>bash</code>, etc.)</li></ol><p>For instance, on macOS with Homebrew and ZSH:</p><pre><code class="language-bash"># Using Homebrew on macOS
brew install asdf
echo -e &quot;\\n. $(brew --prefix asdf)/libexec/asdf.sh&quot; &gt;&gt; ${ZDOTDIR:-~}/.zshrc
</code></pre><h3 id="setup">Setup</h3><p>Add the Python plugin:</p><pre><code class="language-bash"># asdf plugin add &lt;name&gt;: Adds a plugin for managing a specific runtime
# e.g. &lt;name&gt;: python, nodejs, golang
asdf plugin add python
</code></pre><h3 id="basic-usage">Basic Usage</h3><p>Let&#x2019;s install our first Python version:</p><pre><code class="language-bash"># asdf install &lt;name&gt; &lt;version&gt;: Installs a specific version of a runtime
asdf install python 3.12.1
</code></pre><p>What if most of your projects rely on Python 3.12.1? Well, let&#x2019;s set Python 3.12.1 as our global/default Python version:</p><pre><code class="language-bash"># asdf global &lt;name&gt; &lt;version&gt;: Sets a global (default) version of a runtime
asdf global python 3.12.1
cd &amp;&amp; python --version # Python 3.12.1
</code></pre><p>Next, let&#x2019;s install more Python versions!</p><pre><code class="language-bash">asdf install python 3.8.13
asdf install python 3.9.16
asdf install python 3.10.9
asdf install python 3.11.3
</code></pre><p>Wait, I lost track, how many different Python versions have I installed&#x2026;?</p><pre><code class="language-bash">asdf list python
#  3.10.9
#  3.11.3
# *3.12.1
#  3.8.13
#  3.9.16</code></pre><p>&quot;<code>*3.12.1</code>&quot; in the <code>asdf list python</code> output indicates that 3.12.1 is the currently active (local) Python version in the current directory.</p><p>Now, you can easily switch between different Python versions:</p><pre><code class="language-bash"># Go to my project
cd ~/github.com/ngshiheng/burplist

# Project current Python version
python --version # Python 3.12.1

# But, I need Python 3.8.13
asdf local python 3.8.13

# Yay!
python --version # Python 3.8.13
</code></pre><p>That&#x2019;s it! You&apos;ll likely find yourself using this set of commands about 80% of the time.</p><h3 id="cheatsheet">Cheatsheet</h3><p>Here&apos;s a quick refresher:</p><pre><code class="language-bash"># &quot;How to install a specific Python version?&quot;
asdf install python 3.12.1

# &quot;What versions have I installed?&quot;
asdf list python

# Set version on global level
asdf global python 3.12.1

# Set version on project level
asdf local python 3.12.1

# &quot;What is my current Python version in this dir?&quot;
python --version
</code></pre><h2 id="tools-version"><code>.tools-version</code></h2><p>Now you may notice that your project directory may contain a file called <code>.tool-versions</code>.  It&apos;s used to remember which versions of these tools each project needs (<a href="https://asdf-vm.com/manage/configuration.html#tool-versions">reference</a>).</p><h3 id="should-i-commit-this-file-to-git">Should I commit this file to Git?</h3><p>If having the <code>.tool-versions</code> file in your project helps everyone on your team use the same versions of tools, then it&apos;s a good idea to include it in your source control. It keeps things consistent for everyone.</p><h2 id="not-just-python">Not Just Python</h2><p>This approach isn&apos;t limited to Python; it works for managing versions of other tools like Node.js, Go, etc. All you need to do is to replace &quot;<code>python</code>&quot; with the respective tool/plugin name:</p><pre><code class="language-bash"># Same examples, but in golang:
asdf plugin add golang
asdf install golang 1.21.6
asdf list golang
asdf global golang 1.21.6
asdf local golang 1.21.6
go version

# Same examples, but in nodejs:
asdf plugin add nodejs
asdf install nodejs 21.6.1
asdf list nodejs
asdf global nodejs 21.6.1
asdf local nodejs 21.6.1
node --version
</code></pre><h2 id="closing-thought">Closing Thought</h2><p>These days, when considering adopting a new tool, I&apos;ve adopted a systematic approach:</p><ol><li>Firstly, I check <code>asdf</code> to see if there&apos;s plugin support available (<code>asdf plugin list all</code>). Vet the plugin first!</li><li>If not, I explore whether the specific CLI tool has its own version manager like <code>gvm</code>, <code>nvm</code>, <a href="https://github.com/rbenv/rbenv"><code>rubyenv</code></a>, <code>pyenv</code>, <a href="https://github.com/tfutils/tfenv"><code>tfenv</code></a>, etc.</li><li>If neither option is viable, then only I resort to installing the tool from the source via my package manager like <code>brew</code> or <code>apt</code></li></ol><p>Following this decision-making chain has significantly simplified version management for all my tools, saving me considerable time and pain.</p><h3 id="references">References</h3><ul><li><a href="https://github.com/asdf-vm/asdf">https://github.com/asdf-vm/asdf</a></li><li><a href="https://asdf-vm.com/">https://asdf-vm.com/</a></li><li><a href="https://github.com/pyenv/pyenv">https://github.com/pyenv/pyenv</a></li></ul>]]></content:encoded></item><item><title><![CDATA[I Made $920.26 Internet Profit in 2023]]></title><description><![CDATA[<p>Making Internet money is kinda cool. As I wrap up 2023, I decided to jot down the various Internet revenue streams that I have made throughout the year. However, little did I anticipate the nuances involved.</p><p>Ironically, I usually pride myself on being a well-organized person. But honestly, I never</p>]]></description><link>https://jerrynsh.com/i-made-920-internet-profit-in-2023/</link><guid isPermaLink="false">6587fa216213d88b8bc8d88d</guid><category><![CDATA[Tiny Project]]></category><dc:creator><![CDATA[Jerry Ng]]></dc:creator><pubDate>Tue, 02 Jan 2024 00:00:16 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1546271876-af6caec5fae5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fG5ldyUyMHllYXJ8ZW58MHx8fHwxNzAzNDEwMjIwfDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1546271876-af6caec5fae5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fG5ldyUyMHllYXJ8ZW58MHx8fHwxNzAzNDEwMjIwfDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="I Made $920.26 Internet Profit in 2023"><p>Making Internet money is kinda cool. As I wrap up 2023, I decided to jot down the various Internet revenue streams that I have made throughout the year. However, little did I anticipate the nuances involved.</p><p>Ironically, I usually pride myself on being a well-organized person. But honestly, I never anticipated that these inconsequential ventures would bring in any money. So, here I am, realizing I never bother to consolidate everything into a single place. Oops.</p><p>So, I ended up finding myself jumping from one platform to another, navigating through a maze of dashboards. It felt like a digital treasure hunt just to nail down the right numbers.</p><p>Anyway, let&#x2019;s see&#x2026;</p><h3 id="tldr">TL;DR</h3><p>In the year 2023, I made a total profit of <strong>$920.26</strong> (USD):</p><ul><li>Income: $1027.66</li><li>Expenses: -$107.40</li></ul><h2 id="revenue">Revenue</h2><p><strong>Total: $1,027.66</strong></p><h3 id="medium-partner-program">Medium Partner Program</h3><p><strong>Income: $229.70</strong></p><p>All of my latest articles find their first home on <a href="https://jerrynsh.com/"><a href="https://jerrynsh.com/">jerrynsh.com</a></a>. At the same time, they are automatically cross-posted to <a href="https://jerrynsh.medium.com/">Medium.com</a> using Zapier:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/12/Untitled.png" class="kg-image" alt="I Made $920.26 Internet Profit in 2023" loading="lazy" width="654" height="574" srcset="https://jerrynsh.com/content/images/size/w600/2023/12/Untitled.png 600w, https://jerrynsh.com/content/images/2023/12/Untitled.png 654w"><figcaption><span style="white-space: pre-wrap;">Zapier cross-posting automation</span></figcaption></figure><p>So, here&apos;s the revenue breakdown by month in 2024 (cutoff on 28 December 2023):</p><table>
<thead>
<tr>
<th>Month</th>
<th>Revenue ($)</th>
<th>After Tax ($)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Jan</td>
<td>27.20</td>
<td>19.04</td>
</tr>
<tr>
<td>Feb</td>
<td>23.92</td>
<td>16.74</td>
</tr>
<tr>
<td>Mar</td>
<td>13.58</td>
<td>9.51</td>
</tr>
<tr>
<td>Apr</td>
<td>13.62</td>
<td>9.53</td>
</tr>
<tr>
<td>May</td>
<td>15.64</td>
<td>10.95</td>
</tr>
<tr>
<td>Jun</td>
<td>14.98</td>
<td>10.49</td>
</tr>
<tr>
<td>Jul</td>
<td>22.85</td>
<td>15.99</td>
</tr>
<tr>
<td>Aug</td>
<td>9.37</td>
<td>6.56</td>
</tr>
<tr>
<td>Sep</td>
<td>21.36</td>
<td>14.95</td>
</tr>
<tr>
<td>Oct</td>
<td>22.83</td>
<td>15.98</td>
</tr>
<tr>
<td>Nov</td>
<td>15.32</td>
<td>10.72</td>
</tr>
<tr>
<td>Dec</td>
<td>127.49</td>
<td>89.24</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td><strong>328.16</strong></td>
<td><strong>229.70</strong></td>
</tr>
</tbody>
</table>
<p>As you can see, most months didn&apos;t bring in much, except for the final month when a post about <a href="https://jerrynsh.com/a-look-back-on-7-years-of-automating-stuff/">my adventures in automating stuff</a> gained some traction on Medium.</p><p>Oh, It&apos;s worth noting that I am paying a whopping <strong>30% withholding tax</strong>! Ouch.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/12/Untitled--1-.png" class="kg-image" alt="I Made $920.26 Internet Profit in 2023" loading="lazy" width="283" height="151"><figcaption><span style="white-space: pre-wrap;">Screenshot from my Medium Partner Program dashboard</span></figcaption></figure><p>As I was documenting everything, I started to wonder how this compare to last year. Turns out, I&#x2019;m <strong>down by 64.5%. </strong>Yeah, a substantial drop from last year. Oh well.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/12/Untitled--2-.png" class="kg-image" alt="I Made $920.26 Internet Profit in 2023" loading="lazy" width="436" height="339"><figcaption><span style="white-space: pre-wrap;">My Stripe dashboard (excluding December&apos;s earnings)</span></figcaption></figure><p>Overall, I think Medium is not a bad distribution channel for people who already write.</p><h3 id="google-adsense">Google AdSense</h3><p><strong>Income: $117.73 </strong>(~$155.94 SGD)</p><p>If you&apos;ve read some of the posts on jerrynsh.com, you may have come across some ads (unless you&apos;re using an ad-blocker, of course).</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/12/Untitled--3-.png" class="kg-image" alt="I Made $920.26 Internet Profit in 2023" loading="lazy" width="1287" height="177" srcset="https://jerrynsh.com/content/images/size/w600/2023/12/Untitled--3-.png 600w, https://jerrynsh.com/content/images/size/w1000/2023/12/Untitled--3-.png 1000w, https://jerrynsh.com/content/images/2023/12/Untitled--3-.png 1287w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Just a glimpse of my AdSense dashboard</span></figcaption></figure><p>Obviously, the earnings numbers don&apos;t mean a thing on their own.</p><p>Looking at the views, I&apos;m hitting the <strong>11k</strong> mark every month recently. It&apos;s kind of wild to think people would want to hang out here on my little corner of the Internet.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/12/Untitled--4-.png" class="kg-image" alt="I Made $920.26 Internet Profit in 2023" loading="lazy" width="709" height="356" srcset="https://jerrynsh.com/content/images/size/w600/2023/12/Untitled--4-.png 600w, https://jerrynsh.com/content/images/2023/12/Untitled--4-.png 709w"><figcaption><span style="white-space: pre-wrap;">Views (2023 vs 2022)</span></figcaption></figure><p>Beyond that, you guys are spending an <strong>average of</strong> <strong>1 minute and 45 seconds</strong> reading stuff here on this blog.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/12/Screenshot-2023-12-28-at-9.34.52-AM.png" class="kg-image" alt="I Made $920.26 Internet Profit in 2023" loading="lazy" width="715" height="412" srcset="https://jerrynsh.com/content/images/size/w600/2023/12/Screenshot-2023-12-28-at-9.34.52-AM.png 600w, https://jerrynsh.com/content/images/2023/12/Screenshot-2023-12-28-at-9.34.52-AM.png 715w"><figcaption><span style="white-space: pre-wrap;">Engagement Time (2023 vs 2022)</span></figcaption></figure><p>About 70% of this traffic comes from organic search, which I think is great.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/12/Untitled--5-.png" class="kg-image" alt="I Made $920.26 Internet Profit in 2023" loading="lazy" width="855" height="388" srcset="https://jerrynsh.com/content/images/size/w600/2023/12/Untitled--5-.png 600w, https://jerrynsh.com/content/images/2023/12/Untitled--5-.png 855w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Traffic Source (2023 vs 2022)</span></figcaption></figure><p>Now, full disclosure, I&apos;m not a huge fan of <em>most</em> ads, but they pull their weight by helping cover my costs for my <a href="https://jerrynsh.com/tag/tiny-project/">tiny projects</a>.</p><p>Having said that, I&apos;m eyeing a switch to <a href="https://www.ethicalads.io/">EthicalAds</a> or <a href="https://www.carbonads.net/">Carbon Ads</a> next year. The plan is to bring in more relevant and less intrusive ads.</p><h3 id="affiliatereferrals-rewards"><strong>Affiliate/Referrals Rewards</strong></h3><p><strong>Income: $635.00</strong></p><p>This year, somehow, by some dumb luck, I&apos;ve managed to pull in some decent cash through affiliate/referral links that were scattered in my blog posts a year or two ago:</p><ol><li><a href="https://prf.hn/click/camref:1011l3AzrH">Nium</a> &#x2014; $100.00</li><li><a href="https://www.scraperapi.com/?fp_ref=jerryngg">ScraperAPI</a> &#x2014; $535.00</li></ol><p>I reached out to Nium around 2-3 years ago, but I haven&apos;t been writing much about them since the partnership started. Most of them came from a single blog post that I wrote back in Jan 2020. </p><p>Nonetheless, I genuinely like their service for money transfers compared to the traditional banking hassle and fees for foreign transactions. Although, I&apos;m not sure how long this revenue stream will keep flowing.</p><p>On the other hand, the money brought in by ScraperAPI is quite decent this year. Though, most of my earnings from them come from one loyal user (talk about putting all your eggs in one basket).</p><p>I&apos;ll be honest, the sustainability of this income source seems a bit iffy. We&apos;ll see how it goes.</p><h3 id="tournacat"><strong>Tournacat</strong></h3><p><strong>Income: $40.23</strong></p><p>Let me introduce you to <a href="https://tournacat.com/">Tournacat</a>, my little brainchild from this year. It&apos;s a simple <a href="https://workspace.google.com/marketplace/app/tournacat/1041160187344">Google Calendar workspace add-on</a> that syncs upcoming Esports matches/tournaments right to your Google Calendar.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/12/Untitled-1.png" class="kg-image" alt="I Made $920.26 Internet Profit in 2023" loading="lazy" width="1000" height="429" srcset="https://jerrynsh.com/content/images/size/w600/2023/12/Untitled-1.png 600w, https://jerrynsh.com/content/images/2023/12/Untitled-1.png 1000w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">How Tournacat Pro created Esports calendars and events look like.</span></figcaption></figure><p>Seriously, if you&apos;re into Esports, you should totally give it a try &#x2014; you won&apos;t be disappointed!</p><p>Tournacat is free to use from the get-go. But, if users feel a bit fancy and opt for the Pro plan for just $2.50/month (which comes with a <a href="https://www.investopedia.com/updates/purchasing-power-parity-ppp/">purchasing power parity</a> discount too!), they can unlock some <a href="https://store.tournacat.com/checkout">pretty neat features</a>.</p><p>Oh, sorry for the sales pitch &#x2014; here&#x2019;s the breakdown of how much it made:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/12/Untitled--1--1.png" class="kg-image" alt="I Made $920.26 Internet Profit in 2023" loading="lazy" width="489" height="480"><figcaption><span style="white-space: pre-wrap;">Lemon Squeezy payout page</span></figcaption></figure><p>For now, I&apos;m letting it grow organically. As I mentioned in my <a href="https://jerrynsh.com/a-look-back-on-7-years-of-automating-stuff/#tournacat-sync-esports-schedules-to-google-calendar"><a href="https://jerrynsh.com/a-look-back-on-7-years-of-automating-stuff/#any-future-plans">previous blog post</a></a>, as long as the cost of growth is covered, I&apos;m okay with giving away free stuff.</p><h3 id="donations">Donations</h3><p><strong>Income: $5.00</strong></p><p>After years of setting up <a href="https://ko-fi.com/jerrynsh">Ko-fi</a>, I finally got my first donation. I actually did end up buying myself a cup of coffee that day.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/12/Untitled--2--1.png" class="kg-image" alt="I Made $920.26 Internet Profit in 2023" loading="lazy" width="610" height="356" srcset="https://jerrynsh.com/content/images/size/w600/2023/12/Untitled--2--1.png 600w, https://jerrynsh.com/content/images/2023/12/Untitled--2--1.png 610w"><figcaption><span style="white-space: pre-wrap;">Donation email from Ko-fi</span></figcaption></figure><h2 id="costexpenses"><strong>Cost/Expenses</strong></h2><p><strong>Total: -$107.40</strong></p><h3 id="domains"><strong>Domains</strong></h3><p><strong>Cost: -$29.64</strong></p><p>So far, I&apos;ve only got 3 domains in my collection: <a href="https://jerrynsh.com/">jerrynsh.com</a>, <a href="http://tournacat.com/">tournacat.com</a>, and <a href="https://burplist.com">burplist.com</a>. Each of them set me back $9.88, summing up to $29.64 for the year.</p><p>I&apos;ve hitched my domain registration and DNS wagon to Cloudflare. Their all-in-one service suits me well, making management a bit easier.</p><h3 id="hosting"><strong>Hosting</strong></h3><p><strong>Cost: -$77.76</strong></p><p>After <a href="https://jerrynsh.com/bid-farewell-to-heroku-free-tier/">making the shift from Heroku</a>, my projects now call <em>three</em> <em>different</em> Platform-as-a-Service (PaaS) homes. The good news? Most of them still don&apos;t cost me anything <em>yet</em>. The hassle is well worth it I suppose.</p><p>However, there&apos;s one exception &#x2014; a <a href="https://m.do.co/c/afdb6bd48884" rel="noreferrer">Digital Ocean</a> droplet that&apos;s currently setting me back $6.48/month. Do the math for a year, and we&apos;re looking at $77.76 (taxes included).</p><p>Looking down the road, I do expect to start paying for Cloudflare Workers because of Tournacat. I really like them, but yeah, we&apos;ll see how that unfolds.</p><h2 id="what-are-you-doing-with-these-profits">What are you doing with these profits?</h2><p>The responsible thing would be to invest in the stock market or something. But nahhhhhh... </p><p>Well, buying coffee seems like a solid plan, right? Isn&apos;t that the point of Ko-fi? Jokes aside. Honestly, no concrete plans. Maybe snag a few more domains for some ideas at the back of my head.</p><p>On a side note, I did manage to snatch a couple of games that run well on the Steam Deck. I&apos;m really excited to play them!</p><h2 id="looking-into-2024">Looking into 2024</h2><p>So, the big question: Do I expect profit/revenue to shoot up? Probably not.</p><p>Why? Well, most (if not all) of the revenue sources are inconsistent &#x2014; take those affiliate links, for example. Without those, the profit numbers would have been down from last year.</p><p>Another chunk of the revenue is tied to this blog post (plus cross-posting on Medium). Growing blog traffic is a challenge for me because I don&apos;t bother much with SEO or self-promotion on social sites like Twitter, LinkedIn or Facebook.</p><h2 id="closing-thoughts">Closing Thoughts</h2><p>This blog was largely inspired by a read at &#x201C;<a href="https://xeiaso.net/blog/blog-profit-2022/">Xe&apos;s blog made $2564.42 in profit last year</a>&#x201D;.</p><p>If my younger self were to ask myself about starting a blog solely for the extra cash, I&apos;d say think twice. As you can see, it&apos;s not the most profitable idea for the most part. The return on investment (time and effort) is very tough to justify, financially.</p><p>I think what kept me going was the fun I got out of writing stuff and the opportunity to talk to random people on the Internet.</p><h3 id="learnings">Learnings</h3><p>I&apos;m no hero, but this year taught me that I&apos;m genuinely happy when people use or read the stuff I made &#x2014; whether it&apos;s a blog, software, or projects. Even if no money is rolling in. So please keep sending your random <a href="https://jerrynsh.com/contact/">DMs/requests</a> my way.</p><p>Earlier in my career, I always thought everything I invested time and effort in must somehow turn into some form of financial gain; otherwise, I was just wasting my time. I&apos;m glad and grateful that I don&apos;t hold on to that anymore.</p><p>Anyway, thanks for reading! Happy New Year!</p>]]></content:encoded></item><item><title><![CDATA[A Look Back on 7 Years of Automating Stuff]]></title><description><![CDATA[<p>A little bit more than 7 years into my career, I thought it would be fun to pen down a summary post about my adventure in automating various tiny aspects of my life. Most if not all of the stuff here sprouted from my own problems and itches I needed</p>]]></description><link>https://jerrynsh.com/a-look-back-on-7-years-of-automating-stuff/</link><guid isPermaLink="false">6556c8df484aac914d6efd93</guid><category><![CDATA[Tiny Project]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[Jerry Ng]]></dc:creator><pubDate>Mon, 04 Dec 2023 00:00:34 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1515162816999-a0c47dc192f7?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fHJlZmxlY3Rpb258ZW58MHx8fHwxNzAwMTg2Mzk2fDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1515162816999-a0c47dc192f7?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fHJlZmxlY3Rpb258ZW58MHx8fHwxNzAwMTg2Mzk2fDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="A Look Back on 7 Years of Automating Stuff"><p>A little bit more than 7 years into my career, I thought it would be fun to pen down a summary post about my adventure in automating various tiny aspects of my life. Most if not all of the stuff here sprouted from my own problems and itches I needed to scratch.</p><p>This post will probably read more like a personal diary of the minor nuances that I encountered and the sweet minutes/hours that I managed to snatch back through automation.</p><p>On to the first one &#x2013;</p><h2 id="six-percent-automating-asnb-purchases">Six Percent: Automating ASNB Purchases</h2><p><em>2018 &#x2013; 2022</em></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2024/02/sixpercent.png" class="kg-image" alt="A Look Back on 7 Years of Automating Stuff" loading="lazy" width="449" height="253"><figcaption><span style="white-space: pre-wrap;">The startup window of the bot. This was the first thing that I&apos;ve made that people actually use</span></figcaption></figure><p>For context, ASNB a unit trust management company offers a <a href="https://www.asnb.com.my/asnbv2_2funds_EN.php#hargatetap">fixed-price fund</a> (i.e. it can never go up or down!) that promised a sweet <strong>6%</strong> p.a. dividend back then. It&#x2019;s virtually risk-free.</p><p>Well, there was a catch. The limited funds pool meant that it was selling like hotcakes &#x2014; you had to keep retrying to snag those units. So, what did I do? I wrote this.</p><h3 id="cost">Cost</h3><p>As this turns out to be a tiny Windows executable, it didn&#x2019;t really cost me any money to host or anything.</p><h3 id="free-lunch-literally">Free lunch, literally</h3><p>Beyond saving me countless mindless hours clicking like a headless chicken, I got a free thank-you meal out of it.</p><p>Fast forward to today, and I&apos;m no longer actively using or maintaining the project. Over the years, there have been a few minor bug fixes here and there, but it&apos;s essentially retired. I can&apos;t guarantee that it still works, but man, it saved me so many hours.</p><h3 id="what-did-i-learn">What did I learn</h3><p>Today, I&#x2019;ve become quite comfortable with any form of browser automation. If I can interact with it on the web, I can automate it. The script opened doors for tackling repetitive/mundane tasks, aiding in integration testing, etc.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Years later, I stumbled upon <a href="https://github.com/go-rod/rod" rel="noopener noreferrer">go-rod</a> which is significantly better in terms of ease of use and developer experience.</div></div><p>That aside, I did also learn other cool tricks like how to solve a CAPTCHA using <a href="https://en.wikipedia.org/wiki/Optical_character_recognition">OCR</a> and <a href="https://jerrynsh.com/how-to-package-python-selenium-applications-with-pyinstaller/">how to package a Python app using PyInstaller</a>!</p><h2 id="todoleet-daily-leetcode-questions-in-todoist">Todoleet: Daily Leetcode Questions in Todoist</h2><p><em>2021 &#x2013; Present</em></p><p>Back in early 2021, I started to solve the LeetCode Daily challenge as part of my morning routine. Quite frankly, it wasn&#x2019;t exactly fun for me; it was necessary.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/11/image.png" class="kg-image" alt="A Look Back on 7 Years of Automating Stuff" loading="lazy" width="856" height="296" srcset="https://jerrynsh.com/content/images/size/w600/2023/11/image.png 600w, https://jerrynsh.com/content/images/2023/11/image.png 856w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">My personal Todolist. Nope, I&apos;m not attempting this one.</span></figcaption></figure><h3 id="how-it-works">How it works</h3><p>Even though I&apos;ve ditched my morning LeetCode routine, Todoleet is still up and running today.</p><p>Under the hood, it&#x2019;s merely <a href="https://github.com/ngshiheng/todoleet/blob/main/index.js">a simple JS script</a> that talks to the undocumented LeetCode API and then creates a new to-do task using the Todoist API.</p><p>If you&apos;re looking for the nitty-gritty, I&apos;ve spilled the beans on <a href="https://jerrynsh.com/how-i-sync-daily-leetcoding-challenge-to-todoist/">how I sync the Daily Leetcode Challenge to my Todoist</a>.</p><h3 id="cost-1">Cost</h3><p>$0. The free Cloudflare Worker tier has got me covered! This solution costs 1 request per day and I didn&#x2019;t need to store anything.</p><h3 id="time-saved">Time saved</h3><p>I did manage to save a few clicks (seconds) every day. <em>They all add up, I guess?</em></p><h3 id="any-learnings">Any learnings?</h3><p>This was my introduction to Cloudflare Worker. It paved the road for all the <a href="https://jerrynsh.com/tag/cloudflare-worker/">other projects</a> I&apos;ve tinkered with in my free time!</p><h3 id="future-plans">Future plans</h3><p>I did consider taking this to another level and listing it as a <a href="https://todoist.com/integrations">Todoist integration</a>. But, to be honest, I didn&apos;t care enough to make it happen.</p><h2 id="burplist-sipping-on-craft-beer-savings">Burplist: Sipping on Craft Beer Savings</h2><p><em>2021 &#x2013; Present</em></p><p>Craft beers are delicious. There was just one hiccup &#x2014; the price. Then I figured, wouldn&#x2019;t it be great if I could have current and historical prices of all craft beers in Singapore, all in a place? </p><p>Now, instead of wrestling with 10+ websites for the best deals, a quick search from my database would do the trick. This saves me time and sanity.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/11/image-8.png" class="kg-image" alt="A Look Back on 7 Years of Automating Stuff" loading="lazy" width="892" height="877" srcset="https://jerrynsh.com/content/images/size/w600/2023/11/image-8.png 600w, https://jerrynsh.com/content/images/2023/11/image-8.png 892w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">I was looking for some Dark Ale as I was writing this</span></figcaption></figure><h3 id="how-it-works-1">How it works</h3><p>Burplist is essentially a <a href="https://github.com/ngshiheng/burplist/">web scraper built using Scrapy</a>. It scours over 10 local online stores and e-commerce sites every morning (Singapore time), fetching craft beer prices and storing them in a Postgres database. As a result, I&apos;ve amassed two years of historical craft beer price data in Singapore.</p><h3 id="cost-2">Cost</h3><p>Other than the ~$10/year for the domain name, <a href="https://jerrynsh.com/how-i-built-burplist-for-free/">running Burplist has always been free</a>. After Heroku phased out of its free tier plan:</p><ol><li>The scraper Cron job was moved to <a href="https://northflank.com/">Northflank</a></li><li>The Postgres database is hosted on <a href="https://railway.app/?referralCode=jerrynsh">Railway</a></li><li>The website was moved to <a href="https://www.koyeb.com/">Koyeb</a></li></ol><h3 id="free-beer-for-that-christmas">Free beer for that Christmas</h3><p>So, initially, I gave it a shot to make some money out of this. All I did was put it up for sale on Gumroad, but it didn&apos;t really catch on.</p><p>I also tried reaching out to a few companies through cold emails to see if they&apos;d be interested in partnering up with some affiliate links, but only one replied &#x2014; well, it didn&#x2019;t work out either.</p><p>But hey, no big deal! It&apos;s all good because something awesome still came out of it! <a href="https://www.thirsty.com.sg/">Thirsty</a>, this local craft beer company, actually surprised me with this amazing package of craft beer for Christmas that year! I was over the moon.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/11/image-1.png" class="kg-image" alt="A Look Back on 7 Years of Automating Stuff" loading="lazy" width="576" height="531"><figcaption><span style="white-space: pre-wrap;">I got a box of delicious craft beers for Christmas that year</span></figcaption></figure><h3 id="any-lesson-learned">Any lesson learned?</h3><p>This was quite a big one. I&#x2019;ve had so much fun and learned so much from making this project. Burplist is the <em>fanciest</em> web scraper that I&#x2019;ve built thus far. I&#x2019;ve written down some of the learnings in a <a href="https://jerrynsh.com/5-useful-tips-while-working-with-python-scrapy/">blog post</a>.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F602;</div><div class="kg-callout-text">Oh, and here&apos;s a quirky side effect: I&#x2019;m now able to figure out whether a mega-sale/promotion is the real deal or just a clever markup with a discount disguise.</div></div><h3 id="looking-ahead">Looking ahead</h3><p>Future plans? Maybe migrate from Postgres to SQLite. Then, update the daily job to push the SQLite file directly to GitHub and present the data through something like <a href="https://phiresky.github.io/blog/2021/hosting-sqlite-databases-on-github-pages/">this nifty method</a>. Expected result? One less webserver to babysit. If this ever happens, I&#x2019;ll probably write about it somewhere.</p><h2 id="wraith-automating-ghost-blog-backup">Wraith: Automating Ghost Blog Backup</h2><p><em>2022 &#x2013; Present</em></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/11/image-4.png" class="kg-image" alt="A Look Back on 7 Years of Automating Stuff" loading="lazy" width="485" height="193"><figcaption><span style="white-space: pre-wrap;">A screenshot of my terminal emulator</span></figcaption></figure><p>At the start of my blogging journey, I didn&#x2019;t really think too much about what would happen if this $6/month Droplet crashed. I mean, I had all my blog entries in Notion, so I figured &quot;Meh, it wouldn&#x2019;t hurt to copy-paste ~10 of them back if anything bad happens&quot;.</p><p>As time went on, the realization hit &#x2014; losing all my data and whatnot now would really suck. So, the solution? Automate the backup. With the blog serving around 10k views monthly, any downtime or a prolonged <code>404</code> or <code>500</code> is something I&apos;d rather avoid.</p><h3 id="what%E2%80%99s-underneath">What&#x2019;s underneath</h3><p>It&#x2019;s a <a href="https://github.com/ngshiheng/wraith/blob/ebefb095fa14ef4d5a5611a1eb3e6c4f70f559d3/backup.sh#L85">pretty simple Bash script</a> that does three things:</p><ol><li><code>ghost backup</code></li><li><code>mysqldump</code></li><li><code>rclone</code> to a remote drive (e.g. Dropbox, Google Drive, etc.)</li></ol><p>This Bash script gets a weekly run in a Cron job, and it&apos;s as easy as that.</p><h3 id="cost-3">Cost</h3><p>$0. I didn&#x2019;t have to pay for Dropbox; the free tier fits my needs just fine.</p><h3 id="time-saved-1">Time saved</h3><p>I mean, the alternative would be manually SSH-ing into my Droplet, running backup steps one by one &#x2014; a process taking roughly about 5 minutes or less. So, that&apos;s the weekly saving of ~5 minutes.</p><h3 id="what-did-i-learn-1">What did I learn</h3><p>Picked up a few best practices for writing Bash script along the way. Besides that, I did learn about new tools like <code>expect</code>, <code>rclone</code>, and <code>pass</code> (the <a href="https://wiki.archlinux.org/title/Pass">Linux password manager</a>).</p><h3 id="future-plans-1">Future plans</h3><p>Not a whole lot to be honest. Perhaps a backup restore script could be handy. Thought about moving passwords from plain text to using <code>pass</code>. But, that means users dealing with <code>gpg</code> + <code>pass</code> CLI setup &#x2014; an extra hurdle.</p><h2 id="tournacat-sync-esports-schedules-to-google-calendar">Tournacat: Sync Esports Schedules to Google Calendar</h2><p><em>Started 2023</em></p><p>I got tired of missing highly anticipated Dota 2 Esports matches, dealing with wonky time zones, and the mental gymnastics of remembering it all. Since Google Calendar is practically my second brain, I figured, why not sync upcoming matches straight into it?</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/12/image-1.png" class="kg-image" alt="A Look Back on 7 Years of Automating Stuff" loading="lazy" width="2000" height="858" srcset="https://jerrynsh.com/content/images/size/w600/2023/12/image-1.png 600w, https://jerrynsh.com/content/images/size/w1000/2023/12/image-1.png 1000w, https://jerrynsh.com/content/images/size/w1600/2023/12/image-1.png 1600w, https://jerrynsh.com/content/images/size/w2400/2023/12/image-1.png 2400w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Month view of Google Calendar</span></figcaption></figure><h3 id="turning-it-into-a-micro-saas">Turning it into a micro SaaS</h3><p>I started by sharing the Dota 2 calendar with friends. Then, a lightbulb moment &#x2014; if it&apos;s handy for them, <em>maybe</em> others would pay for it. And so, <a href="https://jerrynsh.com/sync-dota-2-esports-matches-to-your-google-calendar/">D2GCal was born</a>. Pay, and get a public link to the Google Calendar. Simple.</p><p>But wait &#x2014; people are picky about their calendars. Some find it &#x201C;noisy&#x201D;, and some want a customized experience. Enter <a href="https://workspace.google.com/marketplace/app/tournacat/1041160187344">Tournacat</a>, supporting 10+ Esports titles (not just limited to Dota 2!), giving users the power to own and customize their calendars.</p><h3 id="cost-4">Cost</h3><p>~$10/year for now for the domain name.</p><p>Firstly, the website (<a href="https://tournacat.com/">tournacat.com</a>) operates as a static site built using <a href="https://gohugo.io/">Hugo</a>. It&apos;s currently hosted using Cloudflare Pages which is free.</p><p>The <a href="https://workspace.google.com/marketplace/app/tournacat/1041160187344">Google Workspace add-on</a> is built and runs on Google Apps Script (GAS) for free.</p><p>Lastly, Tournacat has an API server running on Cloudflare Worker. It fetches the upcoming Esports events from a data source. Running on Cloudflare Worker is currently still within the free tier limit but it won&apos;t be so for long.</p><p>The neat part? Tournacat doesn&apos;t store any user info. No names, no emails &#x2014; nothing at all. This means that no database is needed.</p><h3 id="revenue">Revenue</h3><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/11/image-10.png" class="kg-image" alt="A Look Back on 7 Years of Automating Stuff" loading="lazy" width="489" height="480"><figcaption><span style="white-space: pre-wrap;">Lemon Squeezy payout page</span></figcaption></figure><p>Today, Tournacat has about 90 users. In terms of paid subscriptions, it&apos;s made a tiny profit of <em>$40.23 in 11 months</em>.</p><p>Honestly, I never expected to rake in big bucks anyway. The goal was to cover growing worker usage costs. Any surplus? Well, that&apos;s coffee money.</p><h3 id="lessons-learned">Lessons learned</h3><p>The journey with Tournacat has been a blast. The realization that a handful of people want and use what you built yourself is an indescribable feeling.</p><p>This tiny venture has taught me <a href="https://jerrynsh.com/i-built-a-google-calendar-add-on-heres-what-i-learnt/">many valuable lessons</a>:</p><ol><li>Developing on the GAS platform</li><li>Publishing of a Google Calendar add-on to Workspace</li><li>Working with payments and subscriptions</li><li>Crafting a micro SaaS</li></ol><p>Besides, the journey also introduced me to the mundane world of social media marketing, which, isn&apos;t my cup of tea. On the flip side, I had an amazing time speaking to people about their feedback (feel free to ask me anything)!</p><p>Overall, I feel like the Esports landscape is shaped by a generation accustomed to abundant free entertainment and tools. Convincing people to spend money can be a tough sell.</p><h3 id="any-future-plans">Any future plans?</h3><p>There are plenty of tasks/tickets on the project board, but I&apos;m taking it slow. The plan? I&#x2019;ll just be working on minor improvements here and there unless specific user requests pop up.</p><p>Being a solo developer has its perks &#x2014; the turnaround time for new requests is pretty quick. I&apos;ve been able to incorporate feedback almost immediately (as long as it&apos;s somewhat reasonable).</p><p>Overall, the product feels pretty complete as it is.</p><h2 id="sgs-issuance-calendar-automated-t-bill-tracking">SGS Issuance Calendar: Automated T-Bill Tracking</h2><p><em>Started 2023</em></p><p>Feeling the manual-checking fatigue for the Singapore MAS T-bill issuance calendar, I decided, &quot;Why not automate this?&quot; So, I crafted a schedule to run every month using Google Apps Script (GAS) and detailed the process in <a href="https://jerrynsh.com/creating-a-mas-t-bill-calendar/">this blog post</a>.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/11/image-6.png" class="kg-image" alt="A Look Back on 7 Years of Automating Stuff" loading="lazy" width="2000" height="1190" srcset="https://jerrynsh.com/content/images/size/w600/2023/11/image-6.png 600w, https://jerrynsh.com/content/images/size/w1000/2023/11/image-6.png 1000w, https://jerrynsh.com/content/images/size/w1600/2023/11/image-6.png 1600w, https://jerrynsh.com/content/images/2023/11/image-6.png 2098w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">T-bill announcement and auction dates are on my calendar!</span></figcaption></figure><h3 id="how-it-works-2">How it Works</h3><p>Under the hood, it&apos;s a small <a href="https://github.com/ngshiheng/sgs-issuance-calendar">GAS project written in TypeScript</a>. The script gets triggered every month; pings the MAS API, and then creates important dates (e.g. announcement/auction date) as Google Calendar events. Simple as that!</p><h3 id="cost-5">Cost</h3><p>$0. The best part about using GAS is that I don&#x2019;t need to be bothered by the underlying infrastructure hassle. No fretting over scaling, upgrades, backups, availability &#x2014; none of that. It just does its thing.</p><h3 id="some-lessons-learned">Some lessons learned</h3><p>This project brought some learnings to the table. I am now able to build GAS projects in TypeScript and delved into the world of writing unit tests for GAS. This newfound skillset later found a home in Tournacat&apos;s add-on UI codebase, which is also written in GAS.</p><h3 id="whats-next">What&apos;s next?</h3><p>I&apos;ve added support for SGS and SSB calendars as well! For now, no concrete plans unless user feedback or GitHub issues come knocking. Two months in, and it&apos;s been serving me well.</p><h2 id="closing-thoughts">Closing Thoughts</h2><p>The journey has been nothing short of fun. My approach to automating/building usually unfolds like this:</p><ol><li>Identify my own pain point or problem (note it down immediately!); however small it may be. Look for quick wins!</li><li>Prefer leveraging what&apos;s already out there. Check if there&apos;s something in the market for it, like Zapier or IFTTT</li><li>If not, then roll up my sleeves and code/build</li><li>See if I can generalize the solution or approach</li><li>Write down the process somewhere</li><li>Rinse and repeat</li></ol><h3 id="%E2%80%9Caren%E2%80%99t-some-of-these-side-projects">&#x201C;Aren&#x2019;t some of these side projects?&quot;</h3><p>Well, I guess. I just think the term feels so worn out at this point. It&apos;s like, they&apos;re more about automating some parts of my life. Sure, one of them is <a href="https://jerrynsh.com/i-made-920-internet-profit-in-2023/">making coffee money monthly</a>, a bit laughable. The whole idea of &#x201C;hustling&#x201D; just doesn&apos;t quite vibe with me. I&apos;ve realized I just want to do things <a href="https://justforfunnoreally.dev/">just for fun. No, really</a>.</p><p>Thanks for sticking around! &#x1F37B; Here&apos;s to more automation to come!</p><p></p>]]></content:encoded></item><item><title><![CDATA[MAS TBill Calendar: Add to Google Calendar]]></title><description><![CDATA[<p>I finally had enough of manually checking the Monetary Authority of Singapore (MAS) <a href="https://www.mas.gov.sg/bonds-and-bills/auctions-and-issuance-calendar">T-Bill calendar from the official site</a>. Just miss the buying window? Great! now I&apos;ll have to wait for another week or so.</p><p>Sometimes life gets in the way, then you forget, then you miss again.</p>]]></description><link>https://jerrynsh.com/creating-a-mas-t-bill-calendar/</link><guid isPermaLink="false">6548be545da9ab1d36782ca0</guid><category><![CDATA[Tiny Project]]></category><category><![CDATA[Google Apps Script]]></category><dc:creator><![CDATA[Jerry Ng]]></dc:creator><pubDate>Tue, 07 Nov 2023 00:00:39 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1616530834117-9167fb0d8ebc?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGdvb2dsZSUyMGNhbGVuZGFyfGVufDB8fHx8MTY5OTI2NjIwN3ww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1616530834117-9167fb0d8ebc?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDF8fGdvb2dsZSUyMGNhbGVuZGFyfGVufDB8fHx8MTY5OTI2NjIwN3ww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="MAS TBill Calendar: Add to Google Calendar"><p>I finally had enough of manually checking the Monetary Authority of Singapore (MAS) <a href="https://www.mas.gov.sg/bonds-and-bills/auctions-and-issuance-calendar">T-Bill calendar from the official site</a>. Just miss the buying window? Great! now I&apos;ll have to wait for another week or so.</p><p>Sometimes life gets in the way, then you forget, then you miss again. All that cash just sitting there doing nothing. I mean, there must be a better way to handle this, right?</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4AC;</div><div class="kg-callout-text"><i><em class="italic" style="white-space: pre-wrap;">Context: </em></i><a href="https://www.mas.gov.sg/bonds-and-bills/singapore-government-t-bills-information-for-individuals" rel="noreferrer"><i><em class="italic" style="white-space: pre-wrap;">MAS T-bills</em></i></a><i><em class="italic" style="white-space: pre-wrap;"> are considered safe investments. It has lower credit risk than fixed deposits, which makes it an attractive investment option (~3.8% in 2023). Check the historical MAS T-bill yield </em></i><a href="https://www.mas.gov.sg/bonds-and-bills/treasury-bills-statistics" rel="noreferrer"><i><em class="italic" style="white-space: pre-wrap;">here</em></i></a><i><em class="italic" style="white-space: pre-wrap;">.</em></i></div></div><h2 id="the-calendar">The Calendar</h2><p>Here&apos;s the finished product:</p>
<!--kg-card-begin: html-->
<iframe src="https://calendar.google.com/calendar/embed?height=600&amp;wkst=2&amp;bgcolor=%23ffffff&amp;ctz=Asia%2FSingapore&amp;title=MAS%20SGS%20Issuance%20Calendars&amp;src=NDcxNzZkMTIwZWZiM2M4OTM1OTIxZTgxNmM3YzUzMGY4N2ExNmM0NThjNGFiYTQyZjljZWRkNTE4NWZmNDgzM0Bncm91cC5jYWxlbmRhci5nb29nbGUuY29t&amp;src=NzFkNTAyMDBjMDA4OTNiZjAyOWIxYjVhZDdmMjM4OGZkODU0ODA3YzdlMWJmYTFiM2E0OWI5MTNkNjAzMDUwYUBncm91cC5jYWxlbmRhci5nb29nbGUuY29t&amp;src=MWU3MDlkZjY5OTlhOWNjOGIxYTJiYTRlMDQ5ZDBmZDM0ZTBhNjQ5OTRhNTU5MzRmYTMzMzk3NTM0NWE5YjAzMEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t&amp;src=NDY1MjEwOGViZTU5YjAyZmE3MTdkNmM3NzU5MmNkZjcyNmJlNDgwM2NlM2M2ZmJhOTM5ZGY5ZTI3Nzg3YTY3NEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t&amp;color=%23AD1457&amp;color=%23F4511E&amp;color=%23E4C441&amp;color=%230B8043" style="border-width:0" width="800" height="600" frameborder="0" scrolling="no"></iframe>
<!--kg-card-end: html-->
<p>Here are the links to the individual calendars:</p><table>
<thead>
<tr>
<th>Calendar</th>
<th>Link</th>
</tr>
</thead>
<tbody>
<tr>
<td>SGS Bonds Calendar</td>
<td><a href="https://calendar.google.com/calendar/u/0?cid=MWU3MDlkZjY5OTlhOWNjOGIxYTJiYTRlMDQ5ZDBmZDM0ZTBhNjQ5OTRhNTU5MzRmYTMzMzk3NTM0NWE5YjAzMEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t">Add calendar</a></td>
</tr>
<tr>
<td>6-Month T-Bill Calendar</td>
<td><a href="https://calendar.google.com/calendar/u/0?cid=NzFkNTAyMDBjMDA4OTNiZjAyOWIxYjVhZDdmMjM4OGZkODU0ODA3YzdlMWJmYTFiM2E0OWI5MTNkNjAzMDUwYUBncm91cC5jYWxlbmRhci5nb29nbGUuY29t">Add calendar</a></td>
</tr>
<tr>
<td>1-Year T-Bill Calendar</td>
<td><a href="https://calendar.google.com/calendar/u/0?cid=NDcxNzZkMTIwZWZiM2M4OTM1OTIxZTgxNmM3YzUzMGY4N2ExNmM0NThjNGFiYTQyZjljZWRkNTE4NWZmNDgzM0Bncm91cC5jYWxlbmRhci5nb29nbGUuY29t">Add calendar</a></td>
</tr>
<tr>
<td>Savings Bond Calendar</td>
<td><a href="https://calendar.google.com/calendar/u/0?cid=NDY1MjEwOGViZTU5YjAyZmE3MTdkNmM3NzU5MmNkZjcyNmJlNDgwM2NlM2M2ZmJhOTM5ZGY5ZTI3Nzg3YTY3NEBncm91cC5jYWxlbmRhci5nb29nbGUuY29t">Add calendar</a></td>
</tr>
</tbody>
</table>
<div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">These calendars are updated monthly. It will always contain up-to-date information for the year (<a href="https://github.com/ngshiheng/sgs-issuance-calendar" rel="noreferrer">source code</a>).</div></div><h2 id="idea">Idea</h2><h3 id="existing-solution">Existing Solution</h3><p>Before diving into any project, I always like to see if there are existing solutions that might fit the bill. In my search, I stumbled upon <a href="http://ilovessb.com/">ilovessb.com</a>, which supports email notifications.</p><p>That&apos;s a step in the right direction, but there&apos;s still the possibility of missing those email notifications. Plus, I don&#x2019;t really feel like giving away my email.</p><h3 id="google-calendar">Google Calendar</h3><p>Google Calendar notifications on the other hand are pretty neat. It supports device notifications, emails, or both. So yeah, it&#x2019;s a real winner!</p><p>Anyway, I generally start my week by looking at my Google Calendar to see what&apos;s ahead for the week. Naturally, it occurred to me that having the MAS T-bill issuance calendar sync to my Google Calendar would be a neat solution. Why not automate this process?</p><p>The idea is pretty straightforward:</p><ol><li>Get the necessary data (bonds/bills issuance information and dates)</li><li>Generate calendars and events using that data</li><li>Run this on a regular schedule (monthly/yearly)</li></ol><p>This should be an easy one. After all, I&apos;ve already dabbled in <a href="https://jerrynsh.com/i-built-a-google-calendar-add-on-heres-what-i-learnt/" rel="noreferrer">a similar project</a> before.</p><h2 id="goal">Goal</h2><p>The ultimate objective here is to create a calendar that I can easily share with anyone by simply providing them a link. The idea is to offer people the benefits of using this calendar without requiring them to give up their email or name.</p><p>The only potential downside to this is that if I accidentally mess things up and delete these calendars, users might have to subscribe to a new calendar with a new link. But as long as I don&apos;t touch it, it should be smooth sailing (famous last words I guess). Anyway&#x2026;</p><h2 id="data-source">Data Source</h2><p>The first step involves figuring out how to obtain the data we need to create events for the calendar. There are 2 approaches:</p><ol><li>Inspecting the network requests made when visiting the <a href="https://www.mas.gov.sg/bonds-and-bills/auctions-and-issuance-calendar">issuance calendar site</a></li><li>Scraping the site&#x2019;s HTML</li></ol><p>Option 1 is always my preferred method due to its more structured and reliable nature.</p><h3 id="mas-api">MAS API</h3><p>After some digging around the <a href="https://developer.chrome.com/docs/devtools/network/">network tabs</a>, I was able to identify the API calls made, which opened the door for us to fetch the data.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/11/t-bill-1.gif" class="kg-image" alt="MAS TBill Calendar: Add to Google Calendar" loading="lazy" width="600" height="308" srcset="https://jerrynsh.com/content/images/2023/11/t-bill-1.gif 600w"><figcaption><span style="white-space: pre-wrap;">Inspecting the network tabs</span></figcaption></figure><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x26A0;&#xFE0F;</div><div class="kg-callout-text">It&apos;s crucial to remember not to bombard them with unnecessary requests. A little courtesy goes a long way.</div></div><p>Then, I created <a href="https://github.com/ngshiheng/sgs-issuance-calendar/blob/v1.1.2/src/api.ts#L30">a simple class</a> that wraps around the respective MAS API requests. This file contains a class that wraps around the API requests. This makes it possible for us to interact with MAS API and gather the data required for our calendars later on.</p><p>If we want to fetch other data from other endpoints in the future, we can easily add support for those endpoints. No sweat!</p><h2 id="google-apps-script-gas">Google Apps Script (GAS)</h2><p>The core logic of Cron jobs (i.e. <a href="https://developers.google.com/apps-script/guides/triggers/installable#time-driven_triggers">time-driven triggers</a>), creating calendars, and events is written in TypeScript and deployed as a GAS project. </p><p>You can check out the code <a href="https://github.com/ngshiheng/sgs-issuance-calendar/blob/v1.1.2/src/index.ts">here</a>.</p><h3 id="why-google-apps-script">Why Google Apps Script?</h3><p>Well, there are a few reasons:</p><ol><li>It&apos;s free to run. This is an important point, especially since I plan to maintain this calendar for public use. Ensuring it&apos;s free is essential for its sustainability.</li><li>Its Google Calendar API is super easy to use. No need to handle authentication like you would with its other languages SDKs. Dealing with that kind of stuff is an extra annoyance.</li><li>It can be written in JavaScript/TypeScript, which is a language I&apos;m already familiar with.</li></ol><p>Also, since I haven&apos;t written any GAS project using TypeScript, I saw this as an opportunity to experiment with it. (The developer experience turned out great!)</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4AC;</div><div class="kg-callout-text">Oh, this project was bootstrapped and deployed following <a href="https://github.com/google/clasp/blob/master/docs/typescript.md#quickstart">this Quickstart</a> guide.</div></div><h2 id="challenges">Challenges</h2><p>Oddly enough, I find the most challenging part of this project is to <a href="https://jerrynsh.com/how-to-write-testable-code-in-python/">write tests</a>. Well, more specifically when it comes to writing tests for GAS projects.</p><h3 id="wait-why-bother-writing-tests-for-a-pet-project">Wait, why bother writing tests for a pet project?</h3><p>Well, the number of &#x201C;live&#x201D; <a href="https://jerrynsh.com/tag/tiny-project/">tiny projects</a> that I currently maintain is slowly getting out of hand. I&#x2019;d imagine coming back to this project a year later, trying to refactor, add a small feature, or fix a bug, and thinking, &quot;Will I break this..?&quot;.</p><p>For me, writing tests reduces that mental toil. It provides a certain &#x201C;safety net&#x201D;, assuring me that the changes won&apos;t break existing functionality. It&apos;s an oddly comforting feeling.</p><p>That being said, I always try to strike a balance between testing and maintenance. I believe in writing enough tests to check for regressions, but I also think that <a href="https://grugbrain.dev/#grug-on-testing">testing can become a maintenance burden if taken to an extreme</a>. I don&#x2019;t want to end up in a scenario where I spend more time wrestling with these darn unit tests than actually fixing the bug or adding a feature!</p><h3 id="why-is-it-hard">Why is it hard?</h3><p>Testing a GAS project can be tricky. When you run code locally (e.g. using <a href="https://jestjs.io/docs/getting-started">Jest</a>) with Node.js, it uses the <a href="https://nodejs.org/en">Node.js runtime</a>. However, when you deploy that code as a GAS, it runs in a <a href="https://developers.google.com/apps-script/guides/v8-runtime">V8 runtime</a>. This difference can cause issues when it comes to testing.</p><p>Let&#x2019;s look at an example. First, make sure you have <code>jest</code> and <code>ts-jest</code> installed. If not, you can install them using <code>npm</code> or <code>yarn</code>:</p><pre><code class="language-bash">npm install --save-dev jest ts-jest @types/jest
</code></pre><p>Now, imagine you&apos;ve written a function called <code>createMonthlyTrigger</code>, which, as the name suggests, is used to create a monthly time-driven using the GAS built-in <code>ScriptApp</code> library.</p><p>Here&apos;s the code example:</p><pre><code class="language-typescript">export function createMonthlyTrigger(): GoogleAppsScript.Script.Trigger {
    const triggers = ScriptApp.getProjectTriggers();
    for (const trigger of triggers) {
        const triggerExist = trigger.getHandlerFunction() === main.name;
        if (triggerExist) {
            Logger.log(`Trigger &quot;${main.name}&quot; already exists`);
            return trigger;
        }
    }

    Logger.log(`Creating a new monthly trigger`);
    return ScriptApp.newTrigger(main.name).timeBased().onMonthDay(1).atHour(1).create();
}
</code></pre><p>The catch here is that you can&apos;t simply run <code>jest</code> and expect it to work smoothly.</p><p>Instead, we need to mock the <code>ScriptApp</code> module since it&apos;s a GAS-specific library and not available in a typical Node.js environment. To achieve this, you can use a library like <code>ts-jest</code> with <code>jest</code> to mock <code>ScriptApp</code>.</p><p>Without mock, we will run into errors like <code>ReferenceError: Logger is not defined</code>.</p><p>Here&apos;s an example of how to do it:</p><figure class="kg-card kg-code-card"><pre><code class="language-typescript">import { createMonthlyTrigger } from &quot;../src/index&quot;;

// Create a mock for ScriptApp
const mockScriptApp = {
    getProjectTriggers: jest.fn(),
    newTrigger: jest.fn().mockReturnThis(),
    timeBased: jest.fn().mockReturnThis(),
    onMonthDay: jest.fn().mockReturnThis(),
    atHour: jest.fn().mockReturnThis(),
    create: jest.fn(),
};

// Mock the Logger
const mockLogger = {
    log: jest.fn(),
};

// Mock the global objects
(global as any)[&quot;Logger&quot;] = mockLogger;
(global as any)[&quot;ScriptApp&quot;] = mockScriptApp;

describe(&quot;createMonthlyTrigger&quot;, () =&gt; {
    afterEach(() =&gt; {
        // Clear the mock calls after each test
        jest.clearAllMocks();
    });

    it(&quot;should return an existing trigger if one exists&quot;, () =&gt; {
        // Mock an existing trigger
        mockScriptApp.getProjectTriggers.mockReturnValue([
            {
                getHandlerFunction: jest.fn().mockReturnValue(&quot;main&quot;),
            },
        ]);

        createMonthlyTrigger();
        expect(mockScriptApp.newTrigger).not.toHaveBeenCalled();
    });

    it(&quot;should create a new trigger if none exists&quot;, () =&gt; {
        // Mock no existing triggers
        mockScriptApp.getProjectTriggers.mockReturnValue([]);

        createMonthlyTrigger();
        expect(mockScriptApp.newTrigger).toHaveBeenCalled();
    });
});
</code></pre><figcaption><p><span style="white-space: pre-wrap;">Running </span><code spellcheck="false" style="white-space: pre-wrap;"><span>jest</span></code><span style="white-space: pre-wrap;"> now works! Yay!</span></p></figcaption></figure><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F5E8;&#xFE0F;</div><div class="kg-callout-text">See the full example <a href="https://github.com/ngshiheng/sgs-issuance-calendar/blob/main/tests/index.test.ts" rel="noreferrer">on GitHub</a>.</div></div><h2 id="devops">DevOps</h2><p>These are the stuff that I always aim to automate as much as possible.</p><h3 id="continuous-integration-ci">Continuous Integration (CI)</h3><p>Setting up CI for your GAS project is pretty much the same as you&apos;d do for any other Node.js project. There&apos;s nothing special or unconventional about it.</p><p>Take a look at <a href="https://github.com/ngshiheng/sgs-issuance-calendar/blob/v1.1.2/.github/workflows/ci.yml">the <code>ci.yaml</code> file</a>.</p><h3 id="manual-deployment">Manual Deployment</h3><p>I highly recommend using <code>clasp</code> (<a href="https://github.com/google/clasp">GitHub</a>) when working on your GAS project. It&apos;s a game-changer, making your life a whole lot easier. It streamlines the development and deployment process, making it a more enjoyable experience.</p><p>I&apos;ve been thinking about adding a Continuous Deployment (CD) part in the future, which would automate the deployment process. Though, I&apos;m not sure how often I&apos;ll be deploying updates anytime soon. This project feels quite &quot;completed&quot; for now, but who knows?</p><h3 id="release">Release</h3><p>I&apos;m using <a href="https://github.com/semantic-release/semantic-release">semantic-release</a> to automate my <a href="https://github.com/ngshiheng/sgs-issuance-calendar/releases">releases</a>. However, in this project, I&apos;ve disabled the part where it automatically publishes the codebase as an NPM package (I mean it&#x2019;s not a library where you would import). </p><p>You can take a look at the <a href="https://github.com/ngshiheng/sgs-issuance-calendar/blob/v1.1.2/.github/workflows/release.yml"><code>release.yml</code></a> and <a href="https://github.com/ngshiheng/sgs-issuance-calendar/blob/v1.1.2/package.json#L27-L32" rel="noreferrer"><code>package.json</code></a> if you&apos;re curious.</p><h3 id="dependency-update">Dependency Update</h3><p>Just like many of my other projects, I&apos;ve set up automatic dependency updates using Renovate. It&apos;s great for keeping the project&apos;s dependencies fresh and up-to-date with minimal effort on my part.</p><p>The best part? With CI setup and tests, it adds another layer of confidence to make sure that any automated updates don&apos;t pose a big risk to the project&apos;s stability.</p><h2 id="closing-thoughts">Closing Thoughts</h2><p>So, there you have it! This project is short and sweet, but it has been on my to-do list for a while. It&apos;s one of those small, nagging issues that bother me a bit every day, and I&apos;m happy to finally find a solution. </p><p>Thanks for reading!</p>]]></content:encoded></item><item><title><![CDATA[Creating a Spaceflight News Blog with HTMX & JSON API]]></title><description><![CDATA[<p>The other day, I was casually browsing the web and stumbled upon the need for an API for my new little project. Instead of starting from scratch, I thought it would be great if I could find an existing API to build upon. That&apos;s when I came across</p>]]></description><link>https://jerrynsh.com/creating-a-spaceflight-news-blog-with-htmx-and-json-api/</link><guid isPermaLink="false">6500779042049b95d6624bdc</guid><category><![CDATA[HTMX]]></category><category><![CDATA[Tiny Project]]></category><dc:creator><![CDATA[Jerry Ng]]></dc:creator><pubDate>Mon, 02 Oct 2023 00:00:14 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1516850228053-a807778c4e0f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDc0fHxzcGFjZSUyMHNodXR0bGV8ZW58MHx8fHwxNjk2MjA2Mzk1fDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1516850228053-a807778c4e0f?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDc0fHxzcGFjZSUyMHNodXR0bGV8ZW58MHx8fHwxNjk2MjA2Mzk1fDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Creating a Spaceflight News Blog with HTMX &amp; JSON API"><p>The other day, I was casually browsing the web and stumbled upon the need for an API for my new little project. Instead of starting from scratch, I thought it would be great if I could find an existing API to build upon. That&apos;s when I came across this repository consisting of <a href="https://github.com/public-apis/public-apis">public APIs</a>.</p><p>As I was browsing through the list of public APIs, I couldn&apos;t help but think how cool it would be to create a tiny website using some of these APIs. It just seemed like such a fun project that could be quickly put together and shown to others.</p><p>But guess what&apos;s even more exciting? Building this website using the available APIs with <a href="https://htmx.org/">HTMX</a> instead of our typical <a href="https://developer.mozilla.org/en-US/docs/Glossary/SPA">Single-Page Application</a> (SPA) frameworks!</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">You can explore the <a href="https://spaceflight-news.jerrynsh.com/blogs.html">final demo</a> here with the source code on <a href="https://github.com/ngshiheng/spaceflight-news" rel="noopener noreferrer">GitHub</a>.</div></div><h2 id="why">Why</h2><p>Well, hype aside, I find this exciting because of the challenge it presents. It&#x2019;s no secret that HTMX can work with both <a href="https://htmx.org/essays/hypermedia-apis-vs-data-apis/">hypermedia and JSON responses</a>. Although, <a href="https://htmx.org/essays/when-to-use-hypermedia/">HTMX is primarily designed for hypermedia</a>. However, I want to prove that it&apos;s completely possible (and not even that difficult) to use HTMX with JSON API as well.</p><h3 id="json-is-the-most-popular-data-format-for-apis">JSON is the most popular data format for APIs</h3><p>Also, this seems to be a common real-life scenario that many encounter.</p><p>Just imagine someone saying, &quot;Hey, I want to migrate to HTMX but I already have a bunch of JSON APIs. Do I have to change my entire server-side implementation? What a bummer!&quot;.</p><p>So, today, I&#x2019;m going to write about how I came to create a <a href="https://spaceflight-news.jerrynsh.com/">very simple website powered by HTMX, Nunjucks, and Spaceflight News API (SNAPI)</a>. But really you can replicate this with any JSON API of your choice.</p><h3 id="static-sites">Static Sites</h3><p>I love static websites. They are fast, <a href="https://news.ycombinator.com/item?id=26594242">cheap, and super easy to host</a>. The best part? They hardly require any maintenance!</p><p>I really like the idea of serving dynamic content from a static site with just <strong>a single HTML file</strong>. When you combine it with HTMX, it takes away all the hassle of dealing with messy JavaScript code in your HTML files.</p><h2 id="prerequisites">Prerequisites</h2><ul><li>Basic knowledge of HTML and JavaScript</li><li>A code editor with a live server for testing (e.g., Visual Studio Code with the <a href="https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer">Live Server extension</a>) or any code playground like <a href="https://jsfiddle.net/">JSFiddle</a></li></ul><h2 id="goals">Goals</h2><p>By the end of this, you will have built a <a href="https://spaceflight-news.jerrynsh.com/blogs.html">Spaceflight News blog page</a> using HTMX with the following features:</p><ol><li>Displaying blog posts with titles, authors, publication dates, and summaries</li><li>Pagination with a &quot;Load More&quot; button</li><li>Search functionality</li></ol><p>The final outcome would somewhat be akin to this <a href="https://htmx.org/examples/click-to-load/">&#x2019;Click to Load&#x2019; example</a>, but built with an <em>actual</em> JSON API (refer to <a href="https://api.spaceflightnewsapi.net/v4/docs/">SNAPI documentation</a>).</p><h2 id="implementation">Implementation</h2><h3 id="step-1-basic-html-structure">Step 1: Basic HTML Structure</h3><p>Let&apos;s get started with building our web page. The first step is to set up the basic HTML structure. It&apos;s super easy!</p><p>Just copy the provided HTML code into a new HTML file. You can name it something like <code>index.html</code>. Oh, and don&apos;t forget to include the necessary script in the <code>&lt;head&gt;</code> section:</p><figure class="kg-card kg-code-card"><pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
    &lt;head&gt;
        &lt;title&gt;Spaceflight News Blogs&lt;/title&gt;
        &lt;meta charset=&quot;utf-8&quot; /&gt;
        &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot; /&gt;
        &lt;script src=&quot;https://unpkg.com/htmx.org&quot;&gt;&lt;/script&gt;
        &lt;script src=&quot;https://unpkg.com/htmx.org/dist/ext/client-side-templates.js&quot;&gt;&lt;/script&gt;
        &lt;script src=&quot;https://cdn.jsdelivr.net/npm/nunjucks@3.2.4/browser/nunjucks.min.js&quot;&gt;&lt;/script&gt;
        &lt;link
            rel=&quot;stylesheet&quot;
            href=&quot;https://cdn.jsdelivr.net/gh/kognise/water.css@latest/dist/dark.css&quot;
        /&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;h1&gt;Spaceflight News Blog&lt;/h1&gt;
        &lt;p&gt;
            Demo of how the &lt;b&gt;load more&lt;/b&gt; UX pattern works on htmx with JSON
            data APIs! You can find the source code on
            &lt;a
                href=&quot;https://github.com/ngshiheng/spaceflight-news/blob/main/blogs.html&quot;
                &gt;GitHub&lt;/a
            &gt;. Go take a look!
        &lt;/p&gt;
    &lt;/body&gt;
&lt;/html&gt;</code></pre><figcaption><p><a href="https://jsfiddle.net/jerrynsh/w89bvmk4/"><span style="white-space: pre-wrap;">jsfiddle.net/jerrynsh/w89bvmk4/</span></a></p></figcaption></figure><p>Now, before we move on, I want to share some libraries that we&apos;ll be using:</p><ul><li><a href="https://htmx.org/">HTMX</a> for AJAX interactions</li><li><a href="https://mozilla.github.io/nunjucks/">Nunjucks</a> for <a href="https://htmx.org/extensions/client-side-templates/">client-side templates</a></li><li><em>Optional: <a href="https://www.cssbed.com/water.css-dark/"><em>water.css</em></a> classless CSS for basic styling</em></li></ul><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Did I mention that I absolutely love classless CSS? Check out these collections of classless CSS themes <a href="https://www.cssbed.com/">here</a>!</div></div><h3 id="api-schema">API Schema</h3><p>Here, we&#x2019;ll be using the <code>/v4/blogs</code> endpoint. According to the <a href="https://api.spaceflightnewsapi.net/v4/docs/#/blogs">SNAPI docs</a>, we&apos;ve got an example response that we can work with:</p><figure class="kg-card kg-code-card"><pre><code class="language-json">{
  &quot;count&quot;: 123,
  &quot;next&quot;: &quot;http://api.example.org/accounts/?offset=400&amp;limit=100&quot;,
  &quot;previous&quot;: &quot;http://api.example.org/accounts/?offset=200&amp;limit=100&quot;,
  &quot;results&quot;: [
    {
      &quot;id&quot;: 0,
      &quot;title&quot;: &quot;string&quot;,
      &quot;url&quot;: &quot;string&quot;,
      &quot;image_url&quot;: &quot;string&quot;,
      &quot;news_site&quot;: &quot;string&quot;,
      &quot;summary&quot;: &quot;string&quot;,
      &quot;published_at&quot;: &quot;2023-09-12T23:51:31.675Z&quot;,
      &quot;updated_at&quot;: &quot;2023-09-12T23:51:31.675Z&quot;,
      &quot;featured&quot;: true,
      &quot;launches&quot;: [
        {
          &quot;launch_id&quot;: &quot;3fa85f64-5717-4562-b3fc-2c963f66afa6&quot;,
          &quot;provider&quot;: &quot;string&quot;
        }
      ],
      &quot;events&quot;: [
        {
          &quot;event_id&quot;: 2147483647,
          &quot;provider&quot;: &quot;string&quot;
        }
      ]
    }
  ]
}
</code></pre><figcaption><p><span style="white-space: pre-wrap;">NOTE: We won&#x2019;t be using all of the fields in this JSON response</span></p></figcaption></figure><p>If we take a look at the JSON response, it looks like we can make good use of the <code>title</code>, <code>url</code>, <code>summary</code>, and <code>published_at</code> fields to create our blog page.</p><p>The first 10 characters of an <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a> date string format are in the format YYYY-MM-DD. So we&#x2019;ll use the first 10 characters of <code>published_at</code> as our published date later.</p><p>The <code>next</code> and <code>previous</code> keys will come in handy for pagination.</p><h3 id="step-2-client-side-templates">Step 2: Client Side Templates</h3><p>Next, we&apos;ll delve into actually calling the API and displaying the available blog posts on SNAPI.</p><figure class="kg-card kg-code-card"><pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
    &lt;head&gt;
        &lt;!-- omitted for brevity --&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;h1&gt;Spaceflight News Blog&lt;/h1&gt;
        &lt;!-- omitted for brevity --&gt;
        &lt;div hx-ext=&quot;client-side-templates&quot;&gt;
            &lt;div
                hx-get=&quot;https://api.spaceflightnewsapi.net/v4/blogs/&quot;
                hx-trigger=&quot;load&quot;
                nunjucks-template=&quot;blogs-template&quot;
            &gt;
                &lt;template id=&quot;blogs-template&quot;&gt;
                    
                &lt;!-- count --&gt;
                {% if not previous %}
                &lt;i&gt; Found {{ count }} articles.&lt;/i&gt;
                {% endif %}

                &lt;!-- results --&gt;
                {% for blog in results %}
                &lt;div&gt;
                    &lt;hr /&gt;
                    &lt;h2&gt;
                        &lt;a href=&quot;{{ blog.url }}&quot; target=&quot;_blank&quot;
                            &gt;{{ blog.title }}&lt;/a
                        &gt;
                    &lt;/h2&gt;
                    &lt;h4&gt;
                        Published by {{ blog.news_site }}, {{
                        blog.published_at | truncate(10, true, &quot;&quot;)}}
                    &lt;/h4&gt;
                    &lt;p&gt;{{ blog.summary }}&lt;/p&gt;
                &lt;/div&gt;
                {% endfor %}
                    
                &lt;/template&gt;
              &lt;div id=&quot;result&quot;&gt;&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/body&gt;
&lt;/html&gt;
</code></pre><figcaption><p dir="ltr"><a href="https://jsfiddle.net/jerrynsh/w87Lc4v5/3/"><span style="white-space: pre-wrap;">jsfiddle.net/jerrynsh/w87Lc4v5/3/</span></a></p></figcaption></figure><p>Now, let me break it down a bit:</p><ol><li>We use the <code>hx-ext=&quot;client-side-templates&quot;</code> attribute to activate the client-side templates extension for HTMX. This little beauty allows us to employ a templating engine (e.g. <a href="https://mozilla.github.io/nunjucks/templating.html">Nunjucks</a>) to transform the JSON response from the API into HTML before it shows up on the page.</li><li>The <code>nunjucks-template</code> attribute specifies the name of the Nunjucks template (<code>blogs-template</code>) we&apos;ll be using to convert the JSON response from the API into HTML. This template comprises the following elements:</li><li>A count of the total number of results</li><li>A list of the results, each featuring a title, news site, published date, and summary</li><li>When the <code>hx-get</code> event is triggered on <code>load</code>, the <code>https://api.spaceflightnewsapi.net/v4/blogs/</code> endpoint is called and the response is parsed as JSON. The Nunjucks template is then used to render the JSON response into HTML. The rendered HTML is then swapped into the DOM, replacing the <code>div</code> element with the ID <code>blogs-template</code>.</li></ol><p><em>Now, <a href="https://jsfiddle.net/jerrynsh/w87Lc4v5/3/" rel="noreferrer"><em>let&apos;s take a peek</em></a> at all this in action on our browser... Huh, it should have worked smoothly, right? Why is the blog section of our page completely blank?! Where are the blog posts?!?!</em></p><h3 id="debugging-cors-workaround">Debugging: CORS Workaround</h3><p>Let&#x2019;s check this out with some good old browser inspection. Checking out the Console tab, we see an error message stating:</p><figure class="kg-card kg-code-card"><pre><code>Access to XMLHttpRequest at &apos;https://api.spaceflightnewsapi.net/v4/blogs/&apos; from origin &apos;https://fiddle.jshell.net&apos; has been blocked by CORS policy: Request header field hx-request is not allowed by Access-Control-Allow-Headers in preflight response.
</code></pre><figcaption><p><span style="white-space: pre-wrap;">Error on the Console tab</span></p></figcaption></figure><p>Ugh! It seems to be a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">CORS</a> error. A bit of Googling led me to a helpful <a href="https://github.com/bigskysoftware/htmx/issues/779">GitHub issue</a>. After a bit of reading, I found out that there&apos;s an <a href="https://htmx.org/events/#htmx:configRequest">event listener for <code>htmx:configRequest</code></a> that we could use.</p><p>We need to work around this issue because the Spaceflight News API doesn&apos;t allow cross-origin requests by default.</p><p>Here&apos;s the script to add to the <code>&lt;head&gt;</code> section to implement the CORS workaround when making requests to external APIs with HTMX. It essentially just removes any custom headers:</p><figure class="kg-card kg-code-card"><pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
    &lt;head&gt;
        &lt;!-- omitted for brevity --&gt;
        &lt;script&gt;
            // CORS workaround
            document.addEventListener(&quot;htmx:configRequest&quot;, (evt) =&gt; {
                evt.detail.headers = [];
            });
        &lt;/script&gt;
    &lt;/head&gt;
    &lt;body&gt;
       &lt;!-- omitted for brevity --&gt;
    &lt;/body&gt;
&lt;/html&gt;
</code></pre><figcaption><p dir="ltr"><a href="https://jsfiddle.net/jerrynsh/jszekt4u/4/"><span style="white-space: pre-wrap;">jsfiddle.net/jerrynsh/jszekt4u/4/</span></a></p></figcaption></figure><p>We can now see a list of blog posts being rendered on our HTML page! Yay!</p><p>However, we are only seeing the results from the first page. Our next step is to work on the pagination part: add a &quot;Load More&quot; button at the bottom of the page.</p><h3 id="step-3-pagination">Step 3: Pagination</h3><p>The &quot;Load More&quot; button is implemented as a <code>&lt;button&gt;</code> element:</p><figure class="kg-card kg-code-card"><pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
    &lt;head&gt;
        &lt;!-- omitted for brevity --&gt;
    &lt;/head&gt;
    &lt;body&gt;
        &lt;h1&gt;Spaceflight News Blog&lt;/h1&gt;
        &lt;!-- omitted for brevity --&gt;
        &lt;div hx-ext=&quot;client-side-templates&quot;&gt;
            &lt;div
                hx-get=&quot;https://api.spaceflightnewsapi.net/v4/blogs/&quot;
                hx-trigger=&quot;load&quot;
                nunjucks-template=&quot;blogs-template&quot;
            &gt;
                &lt;template id=&quot;blogs-template&quot;&gt;
                    
                  &lt;!-- omitted count for brevity --&gt;
  
                  &lt;!-- omitted results for brevity --&gt;
  
                  &lt;!-- load more --&gt;
                  {% if next %}
                  &lt;button
                      hx-get=&quot;{{ next }}&quot;
                      hx-swap=&quot;outerHTML&quot;
                      hx-trigger=&quot;click&quot;
                      nunjucks-template=&quot;blogs-template&quot;
                  &gt;
                      Load More...
                  &lt;/button&gt;
                  {% endif %}
          
                &lt;/template&gt;
                &lt;div id=&quot;result&quot;&gt;&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/body&gt;
&lt;/html&gt;
</code></pre><figcaption><p dir="ltr"><a href="https://jsfiddle.net/jerrynsh/dyf0x18n/3/" rel="noreferrer"><span style="white-space: pre-wrap;">jsfiddle.net/jerrynsh/dyf0x18n/3/</span></a><span style="white-space: pre-wrap;">. Scroll to the bottom of the page to see the button!</span></p></figcaption></figure><p>When the &#x201C;Load More&#x201D; button is clicked, here&#x2019;s what happens:</p><ol><li>The <code>hx-get</code> event is triggered. The <code>next</code> variable is used to specify the URL of the next page of blogs. The HTML that gets swapped into the DOM is the same as the one for the <code>blogs-template</code> element but with some fancy new blogs added to the end.</li><li>The <code>hx-swap</code> attribute basically tells us how we should swap the new HTML into the DOM. In our case, we&#x2019;re using the <code>outerHTML</code> <a href="https://htmx.org/docs/#swapping">swap mode</a>. This means that the new HTML will replace the entire existing HTML for the <code>blogs-template</code> element.</li><li>Lastly, the <code>hx-trigger</code> attribute tells us which event will trigger the <code>hx-get</code> event. Here, we&apos;re using the good ol&apos; <code>click</code> event. So, whenever you click on that &quot;Load More&quot; button, <code>hx-get</code> event gets triggered and our pagination game begins!</li></ol><p>That&#x2019;s it! We&#x2019;ve now got a &#x201C;Load More&#x201D; pagination implemented for our blog.</p><p><em>We&apos;re not done yet! We need to find the blog post about the <a href="https://news.ycombinator.com/item?id=37233936"><em>Chandrayaan-3 lunar exploration mission</em></a>. So, let&apos;s keep clicking that &quot;Load More&quot; button...and more...and more...</em></p><h3 id="step-4-search-input">Step 4: Search Input</h3><p>Just kidding! Who would want to keep clicking more to look for specific stuff? Thankfully, the <code>/v4/blog</code> of SNAPI supports search queries out of the box. So, we can easily search for the article about the <em>Chandrayaan-3</em> lunar exploration mission.</p><p>Let&apos;s work on modifying the original <code>div</code> to a search <code>input</code>. So, you can easily type in your search keywords and find the articles you&apos;re interested in:</p><figure class="kg-card kg-code-card"><pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;

  &lt;head&gt;
    &lt;!-- omitted for brevity --&gt;
  &lt;/head&gt;

  &lt;body&gt;
    &lt;h1&gt;Spaceflight News Blog&lt;/h1&gt;
    &lt;!-- omitted for brevity --&gt;
    &lt;div hx-ext=&quot;client-side-templates&quot;&gt;
      &lt;!-- search --&gt;
       &lt;input
             id=&quot;search-input&quot;
             autofocus
             hx-get=&quot;https://api.spaceflightnewsapi.net/v4/blogs/&quot;
             hx-indicator=&quot;.htmx-indicator&quot;
             hx-target=&quot;#result&quot;
             hx-trigger=&quot;load delay:100ms, keyup changed delay:500ms, search&quot;
             name=&quot;search&quot;
             nunjucks-template=&quot;blogs-template&quot;
             placeholder=&quot;Search blogs...&quot;
             type=&quot;search&quot;
      /&gt;


      &lt;template id=&quot;blogs-template&quot;&gt;

        &lt;!-- omitted for brevity --&gt;

      &lt;/template&gt;
      &lt;div id=&quot;result&quot;&gt;&lt;/div&gt;

    &lt;/div&gt;
  &lt;/body&gt;

&lt;/html&gt;
</code></pre><figcaption><p><a href="https://jsfiddle.net/jerrynsh/axg84cfL/9/"><span style="white-space: pre-wrap;">jsfiddle.net/jerrynsh/axg84cfL/9/</span></a><span style="white-space: pre-wrap;">. Note that the </span><code spellcheck="false" style="white-space: pre-wrap;"><span>name</span></code><span style="white-space: pre-wrap;"> attribute has to match the API&apos;s query string, i.e. </span><code spellcheck="false" style="white-space: pre-wrap;"><span>/v4/blogs/?search</span></code></p></figcaption></figure><p>So, the <code>&lt;input&gt;</code> element acts as a search bar that uses the <code>hx-get</code> event to load a list of blogs from the API when you type in a search query.</p><p>The <code>hx-get</code> event is triggered by the <code>load</code>,   <code>keyup</code> and <code>search</code> events (<a href="https://htmx.org/attributes/hx-trigger/">reference</a>). The search results are then rendered in the <code>#result</code> element using the <code>blogs-template</code> Nunjucks template.</p><p><em>Finally, I can <a href="https://jsfiddle.net/jerrynsh/axg84cfL/9/" rel="noreferrer"><em>search for</em> &#x201C;C<em>handrayaan&#x201D; related blog posts</em></a>. No more endless clicking &#x2018;Load More&#x2019;!</em></p><h2 id="conclusion">Conclusion</h2><p>We have successfully created a Spaceflight News blog page with live search functionality, dynamic content loading, and client-side templates using HTMX and JSON API.</p><p>So, what&#x2019;s next? You could give the blog page a makeover by incorporating <a href="https://spaceflight-news.jerrynsh.com/articles.html?search=">infinite scrolling, just like this</a>. Or you could add a <a href="https://spaceflight-news.jerrynsh.com/reports.html">next-previous button</a> instead.</p><p>For a better search experience, you could try to <a href="https://github.com/ngshiheng/spaceflight-news/commit/e494bc204e2aafbc2593b59a64087eb737d6b068#diff-fc9666c0c916da47ca4e119e63e3c75fdca7ea35f9aa6483b6997091bc2bc3aa">update the search input to sync with the search query</a> of the URL so that you can do <a href="https://spaceflight-news.jerrynsh.com/blogs.html?search=Chandrayaan-3">something like this</a>.</p><p>Thanks for reading!</p><h2 id="references">References</h2><p>Making this wouldn&#x2019;t be possible without these awesome resources:</p><ul><li><a href="https://htmx.org/docs/">https://htmx.org/docs/</a></li><li><a href="https://mozilla.github.io/nunjucks/">https://mozilla.github.io/nunjucks/</a></li><li><a href="https://api.spaceflightnewsapi.net/v4/docs/">https://api.spaceflightnewsapi.net/v4/docs/</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Python Exception Handling: Patterns and Best Practices]]></title><description><![CDATA[Read about exception handling patterns and their use cases. Learn when to catch and re-raise, raise new exceptions, chain exceptions, and more.]]></description><link>https://jerrynsh.com/python-exception-handling-patterns-and-best-practices/</link><guid isPermaLink="false">64de0f9742049b95d662498e</guid><category><![CDATA[Python]]></category><dc:creator><![CDATA[Jerry Ng]]></dc:creator><pubDate>Fri, 01 Sep 2023 00:00:52 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1594482628048-53865e5a59c4?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDM2fHxlcnJvcnxlbnwwfHx8fDE2OTIyNzQ2MDV8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1594482628048-53865e5a59c4?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDM2fHxlcnJvcnxlbnwwfHx8fDE2OTIyNzQ2MDV8MA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Python Exception Handling: Patterns and Best Practices"><p>When it comes to raising exceptions and exception handling in Python, I&apos;ve often found myself pondering, &quot;Should I re-raise this exception? Or maybe I should raise it from another exception?&quot;</p><p>You see, the thing is, there are a bunch of ways to handle exceptions in Python. We&#x2019;d often just wing it without really grasping the why and when to use these patterns.</p><p>In this little exploration, we&#x2019;re going to uncover the differences behind these different exception-handling patterns with code examples. Today, we&apos;ll be unraveling:</p><ul><li>When to catch and re-raise an exception?</li><li>When to raise a new exception?</li><li>When to chain exception?</li><li>When to avoid using each of the above?</li></ul><p>Enough talk, let&#x2019;s dive right into these exception-raising dilemmas and turn them into informed decisions. As we go through the examples, feel free to copy and paste the code snippet to try it out yourself on something like <code>ipython</code>!</p><h2 id="pattern-1-catch-and-re-raise-exception">Pattern 1: <strong>Catch and Re-Raise</strong> Exception</h2><figure class="kg-card kg-code-card"><pre><code class="language-python">def divide(x=1, y=0):
    try:
        return x / y

    except ZeroDivisionError as e:
        raise e # NOTE: this is almost equivalent to bare `raise`</code></pre><figcaption><p><span style="white-space: pre-wrap;">Running </span><code spellcheck="false" style="white-space: pre-wrap;"><span>divide()</span></code><span style="white-space: pre-wrap;"> gives you a traceback to </span><code spellcheck="false" style="white-space: pre-wrap;"><span>return x / y</span></code><span style="white-space: pre-wrap;">, exactly where the error was from</span></p></figcaption></figure><p>In this pattern, if an exception occurs during the division operation (e.g., division by zero), the <strong>original </strong>exception<strong> </strong>will be re-raised with its traceback.</p><p>As a result, the original exception is propagated all the way up the call stack with its original traceback.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text"><a href="https://realpython.com/python-traceback/#what-is-a-python-traceback">What is Python traceback</a>? Well, it is essentially a record of what the program was doing before it crashed. Tracebacks are particularly useful for debugging.</div></div><h3 id="use-case">Use Case</h3><p><strong>Generally speaking, this is often a good default pattern</strong> for its clarity and preservation of original traceback.</p><p>Why? Preserving traceback information is often considered a good practice as it helps in diagnosing errors effectively and understanding the sequence of events that led to the exception.</p><p>In short, this pattern is used when you want to catch a specific exception (<code>ZeroDivisionError</code> in this case), do something, and then re-raise the exception. It allows you to perform some specific actions before propagating the exception further.</p><h3 id="effectively-the-same-as-not-having-the-try-block-at-all">Effectively the same as not having the <code>try</code> block at all</h3><pre><code class="language-python">def divide(x=1, y=0):
    return x / y</code></pre><p>Wait what?! Then why do we need the <code>try</code> block then?</p><p>Knowing this, the <code>try</code> block in our previous example might suddenly seem redundant. However, you can imagine scenarios where additional logging or error-handling logic would make the <code>try</code> block very useful:</p><pre><code class="language-python">def divide(x=1, y=0):
    try:
        return x / y

    except ZeroDivisionError as e:
        print(&quot;An error occurred while performing the division.&quot;)
        # Log the error, send notifications, retries etc.
        raise e
</code></pre><p>Having that said, if you do not intend to do any additional stuff, feel free to omit the <code>try</code> block.</p><h3 id="avoid">Avoid</h3><p>While this is generally a useful pattern, there can be scenarios where it might not be the best choice.</p><p>For instance, you may want to avoid using this pattern when you want to hide sensitive information or when the traceback contains sensitive data that you don&apos;t want to expose. Here&#x2019;s an example:</p><figure class="kg-card kg-code-card"><pre><code class="language-python">try:
    user = login(email=&quot;foo@example.com&quot;,password=&quot;12345&quot;)
    # Perform some sensitive operations

except InvalidPasswordFormatError as e:
    raise e</code></pre><figcaption><p><span style="white-space: pre-wrap;">In practice, you want to be as ambiguous as possible</span></p></figcaption></figure><p>In this example, if an exception occurs during the sensitive operation, the original exception is re-raised with its traceback. This pattern may expose sensitive information, such as login failure information in the traceback.</p><p>This is often referred to as &quot;leaking&quot; or &quot;revealing&quot; information. Exposing detailed login failure information is bad because it aids attackers. They can exploit specifics to guess usernames and launch targeted attacks.</p><p>To prevent this, it&apos;s better to handle the exception without re-raising it or to raise a new exception with a more generalized error message (see <a href="#pattern-3-raise-new-exception-from-none">Pattern 3</a>).</p><h2 id="pattern-2-raise-new-exception">Pattern 2: Raise New Exception</h2><figure class="kg-card kg-code-card"><pre><code class="language-python">def divide(x=1, y=0):
    try:
        return x / y

    except ZeroDivisionError:
        raise ValueError(&quot;Pattern 2 error.&quot;)</code></pre><figcaption><p><span style="white-space: pre-wrap;">Compared to Pattern 1, you should see two &quot;Traceback (most recent call last)&quot; here</span></p></figcaption></figure><p>In this example, a new exception is raised with a custom message, while preserving the original exception&apos;s traceback. If a <code>ZeroDivisionError</code> occurs, a new <code>ValueError</code> is raised with a custom message.</p><p>The traceback will include <strong>both</strong> the <code>ZeroDivisionError</code> and the <code>ValueError</code> that was raised.</p><h3 id="use-case-1">Use Case</h3><p>This pattern is useful when you want to raise a different (more meaningful) type of exception to indicate a specific error condition. This still allows us to preserve the original exception&#x2019;s traceback.</p><h3 id="avoid-1">Avoid</h3><p>Avoid using this when you need to preserve the original exception.</p><h2 id="pattern-3-raise-new-exception-from-none">Pattern 3: Raise New Exception from None</h2><figure class="kg-card kg-code-card"><pre><code class="language-python">def divide(x=1, y=0):
    try:
        return x / y

    except ZeroDivisionError:
        raise ValueError(&quot;Pattern 3 error.&quot;) from None
</code></pre><figcaption><p><span style="white-space: pre-wrap;">You will not see </span><code spellcheck="false" style="white-space: pre-wrap;"><span>return x / y</span></code><span style="white-space: pre-wrap;"> mentioned in the traceback</span></p></figcaption></figure><p>This pattern is similar to <a href="#pattern-2-raise-new-exception">Pattern 2</a>. But, using <code>from None</code> suppresses the original <code>ZeroDivisionError</code> exception.</p><p>Here, the traceback will <strong>not</strong> include the original <code>ZeroDivisionError</code>, only the <code>ValueError</code> exception and the custom error message raised.</p><h3 id="use-case-2">Use Case</h3><p>Similar to Pattern 2, you would want to use this pattern when you want to raise a new exception with a custom message. </p><p>The difference here is that this will <strong>not</strong> include the traceback of the original exception. It is useful when you want to hide the details of the original exception from the user.</p><h3 id="more-examples">More Examples</h3><p>If you are wrapping a library that throws internal exceptions and you want to present transformed external exceptions to your application users. </p><p>In this scenario, using this pattern is a suitable approach. Here&#x2019;s a simple example:</p><pre><code class="language-python">try:
    # Some library code that might raise an internal exception
    result = library_function(data)

except InternalException as e:
    raise ExternalException(&quot;An error has occurred&quot;) from None
</code></pre><p>In this example, wrapping internal exceptions with external exceptions helps isolate your application code from the specifics of the internal library&apos;s implementation. This can come in handy when the users of your code don&apos;t need to understand or handle the internal exceptions thrown by the library.</p><h3 id="avoid-2">Avoid</h3><p>Avoid using this approach when you (or your users) need to understand the full context of where the original exception occurred and how it led to the new exception.</p><p>Suppressing exceptions can make it more difficult to track down the root cause of an error, so it should only be done when necessary.</p><h2 id="pattern-4-chaining-exception">Pattern 4: Chaining Exception</h2><pre><code class="language-python">def divide(x=1, y=0):
    try:
        return x / y

    except ZeroDivisionError as e:
        raise ValueError(&quot;Pattern 4 error.&quot;) from e
</code></pre><p>Again, the <code>ZeroDivisionError</code> exception is caught and a new <code>ValueError</code> exception is raised with a custom message. </p><p>Though, the <code>from e</code> clause tells Python to pass the original <code>ZeroDivisionError</code> exception as an argument to the new <code>ValueError</code> exception.  As a result:</p><ul><li>This allows the caller of the <code>divide()</code> function to know what the original error was</li><li>The traceback of the original exception (<code>e</code>) will be included in the printed traceback of the newly raised exception</li></ul><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Read more about <a href="https://docs.python.org/3/library/exceptions.html#exception-context">Exception context</a>.</div></div><h3 id="use-case-3">Use Case</h3><p>This pattern is commonly used when you want to raise a <strong>new</strong> exception with a custom message and include the traceback of the original exception as its cause. It is useful when you want to provide both the specific error message and the context of the original exception.</p><p>In terms of best practices &#x2013; it is generally recommended to use the <code>from e</code> syntax when raising a <strong>new</strong> exception from an inner <code>except</code> block. This allows us to preserve the stack trace of the original error, which (again) can be helpful for debugging.</p><h3 id="pattern-2-vs-pattern-4">Pattern 2 vs. Pattern 4</h3><p>&quot;What&apos;s the difference between <a href="#pattern-2-raise-new-exception">Pattern 2</a> vs. <a href="#pattern-4-chaining-exception">Pattern 4</a> then? They seem awfully similar!&quot;</p><p>In Pattern 2, the <code>ZeroDivisionError</code> exception is simply raised (from the line <code>return x / y</code>) without being handled. This means that the caller of the <code>divide()</code> function will <strong>not</strong> be aware of the error.</p><p>In comparison, Pattern 4 is more informative and therefore the better choice in most cases. However, Pattern 2 may be used if the caller of the <code>divide()</code> function does not need to know about the original error.</p><h3 id="more-example">More Example</h3><p>You&apos;re building a file-processing application that uses an external library for reading and processing files. If the library raises an internal <code>FileNotFoundError</code>, you want to raise your own custom exception to provide more context and information to the user.</p><figure class="kg-card kg-code-card"><pre><code class="language-python">class FileProcessingError(Exception):
    def __init__(self, message):
        super().__init__(message)

def process_file(file_path):
    try:
        content = read_file(file_path)
        # Process content...

    except FileNotFoundError as e:
        raise FileProcessingError(&quot;Unable to process file.&quot;) from e

def read_file(file_path):
    # Simulate a file not found error
    raise FileNotFoundError(f&quot;File not found: {file_path}&quot;)

try:
    process_file(&quot;example.txt&quot;)
except FileProcessingError as e:
    print(f&quot;Error: {e}&quot;) # Error: Unable to process file.
    print(&quot;Original Exception:&quot;, e.__cause__) # Original Exception: File not found: example.txt
</code></pre><figcaption><p><span style="white-space: pre-wrap;">Try removing the </span><code spellcheck="false" style="white-space: pre-wrap;"><span>from e</span></code></p></figcaption></figure><p>In this example, the <code>FileProcessingError</code> is raised with the context of the original <code>FileNotFoundError</code>. This provides more information to the user and helps in debugging by maintaining the traceback chain.</p><h3 id="avoid-3">Avoid</h3><p>Avoid using this pattern when you want to hide the details of the original exception or when the original traceback is not needed (see <a href="#pattern-3-raise-new-exception-from-none">Pattern 3</a>) to understand the higher-level error. </p><p>In some cases, preserving both tracebacks can be confusing if not handled carefully.</p><h2 id="summary">Summary</h2><p>Exception handling in Python is about dealing with errors in your code. The best way to handle exceptions often depends on what you want to achieve.</p><p>Anyway, here&#x2019;s a TL;DR of what we went through:</p><ul><li><a href="#pattern-1-catch-and-re-raise-exception">Pattern 1</a> (good default): Re-raises the <strong>same</strong> exception with its original traceback.</li><li><a href="#pattern-2-raise-new-exception">Pattern 2</a> (situational): Re-raises a <strong>new</strong> exception, does not lose original traceback.</li><li><a href="#pattern-3-raise-new-exception-from-none">Pattern 3</a> (situational): Re-raises a <strong>new</strong> exception with a chained exception relationship (<code>from None</code>), but loses the original traceback.</li><li><a href="#pattern-4-chaining-exception">Pattern 4</a> (best): Re-raises a <strong>new</strong> exception with a chained exception relationship (<code>from e</code>), including both the new and the original traceback.</li></ul><h3 id="best-practices">Best Practices</h3><ol><li>Use the <code>from e</code> syntax when raising a <strong>new</strong> exception from an inner <code>except</code> block. This allows us to preserve the stack trace of the original error.</li><li>Do not suppress exceptions unless it is absolutely necessary. Suppressing exceptions can make it more difficult to track down the root cause of an error.</li><li>Use meaningful error messages. The error message should be clear and concise, and it should provide enough information to help the user understand what went wrong.</li><li>Handle all (possible) errors. It is important to handle all possible errors that your code can throw. This will help to prevent your code from crashing unexpectedly.</li></ol><p>Remember, the way you handle exceptions should make your code easy to understand and debug. Always think about what helps you and others know what went wrong and why.</p><p>Besides learning the right way to handle exceptions, it&apos;s just as important to <a href="https://jerrynsh.com/stop-using-exceptions-like-this-in-python/">stop using exceptions like this in Python</a>!</p>]]></content:encoded></item><item><title><![CDATA[Build Your Own: Python PDF to Text]]></title><description><![CDATA[<p>Recently, I found myself facing the need to convert my personal PDF files to text. While there were <a href="https://www.google.com/search?q=pdf+to+text+converter">many existing PDF to text converters</a> available for this task, I couldn&apos;t shake the feeling of unease about uploading my private documents to unknown servers. Who knows what could happen</p>]]></description><link>https://jerrynsh.com/build-your-own-python-pdf-to-text/</link><guid isPermaLink="false">64b69c2942049b95d6624820</guid><category><![CDATA[Python]]></category><dc:creator><![CDATA[Jerry Ng]]></dc:creator><pubDate>Tue, 01 Aug 2023 00:00:46 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1526925539332-aa3b66e35444?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDIyfHxjb2RlfGVufDB8fHx8MTY4OTY1ODc4NXww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1526925539332-aa3b66e35444?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDIyfHxjb2RlfGVufDB8fHx8MTY4OTY1ODc4NXww&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Build Your Own: Python PDF to Text"><p>Recently, I found myself facing the need to convert my personal PDF files to text. While there were <a href="https://www.google.com/search?q=pdf+to+text+converter">many existing PDF to text converters</a> available for this task, I couldn&apos;t shake the feeling of unease about uploading my private documents to unknown servers. Who knows what could happen to them or how secure they truly are?</p><p>I decided to build my own PDF to text converter (<a href="https://raw.githubusercontent.com/ngshiheng/pypdf2txt/main/docs/images/demo.gif">demo</a>). Right from the get-go, I knew this was possible by using only Python.</p><h2 id="getting-started">Getting Started</h2><p>Before that, I must confess, I&apos;m not particularly skilled in frontend development. So, I opted to use <a href="https://www.pyweb.io/">PyWebIO</a>, sparing me the hassle of dealing with HTML and CSS.</p><h3 id="main-dependencies">Main Dependencies</h3><p>To begin, I set up a new project directory named <code>pypdf2txt</code> and initialized it with Git and <a href="https://python-poetry.org/">Poetry</a> (my go-to virtual environment and package manager for Python).</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">See <a href="https://python-poetry.org/docs/#installation">how to install Poetry here</a>. Alternatively, you can always use good ol&#x2019; <a href="https://pypi.org/project/pip/">Pip</a> and <a href="https://docs.python.org/3/library/venv.html">venv</a></div></div><p>I included the necessary dependencies &#x2014; <a href="https://pypi.org/project/pdfminer.six/">pdfminer.six</a> for working with PDFs and <a href="https://pypi.org/project/pywebio/">pywebio</a> for building the web application.</p><p>Lastly, I created a <code>main.py</code> file to hold the core functionality of the converter.</p><pre><code class="language-bash">mkdir pypdf2txt
cd pypdf2txt
git init
poetry init
poetry add pdfminer.six pywebio
touch main.py
</code></pre><h3 id="optional-dev-dependencies">Optional: Dev Dependencies</h3><p>I added <a href="https://pypi.org/project/autopep8"><code>autopep8</code></a> and <a href="https://github.com/astral-sh/ruff"><code>ruff</code></a> (instead of <a href="https://flake8.pycqa.org/"><code>flake8</code></a>) as development dependencies using Poetry. These will help with automatic code formatting and linting:</p><pre><code class="language-bash">poetry add -D autopep8 ruff
</code></pre><h2 id="converting-pdf-to-text">Converting PDF to Text</h2><p><em>NOTE: Throughout this entire walkthrough, we only have one <code>main.py</code> to work with.</em></p><h3 id="adding-site-description">Adding Site Description</h3><p>Let&#x2019;s start with something a little bit more simple, shall we? To render any output to the browser, we can simply use the <code>pywebio.output</code> module (<a href="https://pywebio.readthedocs.io/en/latest/output.html">reference</a>).</p><pre><code class="language-python"># main.py

from functools import partial
from io import BytesIO, StringIO
from pathlib import Path

from pdfminer.high_level import extract_text_to_fp
from pywebio import config, session, start_server
from pywebio.input import file_upload
from pywebio.output import clear, put_buttons, put_code, put_markdown, toast, use_scope
from pywebio.session import download, run_js

def render_description():
    description = &quot;&quot;&quot;
    # Pypdf2: Convert PDF to Text

    A simple Python web service that allows you to convert your PDF documents to text.
    Extract text from PDF files without compromising **privacy**, **security**, and **ownership**.

    ## Features

    -   Converts PDF documents to text
    -   Simple and easy-to-use web interface
    -   Fast and efficient text extraction
    &quot;&quot;&quot;

    put_markdown(description)
</code></pre><p>In the <code>render_description</code> function, I define the app&apos;s description using a <a href="https://www.markdownguide.org/">markdown</a>-formatted string.</p><p>Then, I use <code>put_markdown</code> from the <code>pywebio.output</code> module to display the description in the web interface.</p><h3 id="setting-up-the-web-app">Setting Up the Web App</h3><p>This is the <code>main</code> function that brings everything together:</p><pre><code class="language-bash"># ... (rest of the code)

def main():
    session.run_js(
        &apos;WebIO._state.CurrentSession.on_session_close(()=&gt;{setTimeout(()=&gt;location.reload(), 4000})&apos;,
    )

    render_description()
    # process_pdf() #  TODO: create &amp; uncomment later 

if __name__ == &quot;__main__&quot;:
    start_server(main, port=8080)
</code></pre><p>It starts the PyWebIO server, runs the JavaScript code for <a href="https://pywebio.readthedocs.io/en/latest/cookbook.html#refresh-page-on-connection-lost">auto-reloading the page on session close</a>, renders the initial description of the app, and invokes the <code>process_pdf</code> function to handle the PDF to text conversion.</p><p>Now, when you run the app, it will render the description at the start:</p><pre><code class="language-bash">poetry run python3 main.py
</code></pre><h3 id="file-upload">File Upload</h3><p>Next, I needed to upload the PDF file through the web interface. Let&#x2019;s create a function name <code>process_pdf</code> utilizing the <code>file_upload</code> API. I found a handy example of <a href="https://pywebio.readthedocs.io/en/latest/input.html#pywebio.input.file_upload">file upload in the PyWebIO documentation</a>, and with a bit of copy-pasting, I was well on my way:</p><pre><code class="language-python"># ... (rest of the code)

def process_pdf():
    put_markdown(
        &quot;&quot;&quot;
        ## Convert PDF To Text
        &quot;&quot;&quot;,
    )

    while True:
        pdf_file = file_upload(
            &quot;Select PDF&quot;,
            accept=&quot;application/pdf&quot;,
            max_size=&quot;10M&quot;,
            multiple=False,
            help_text=&quot;sample.pdf&quot;,
        )

        text_output = extract_text_from_pdf(pdf_file) # TODO: work on this later
        text_filename = f&quot;{Path(pdf_file[&apos;filename&apos;]).stem}.txt&quot;

# ... (rest of the code)
</code></pre><p>First, I added a &quot;Convert PDF To Text&#x201D; heading to the UI, making it clear what the app was designed to do.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">As the input function of PyWebIO is blocking and the input form will be destroyed after successful submission, we put everything under a while true loop here so that we always recreate the input. (<a href="https://pywebio.readthedocs.io/en/latest/pin.html#overview">reference</a>)</div></div><p>The <code>file_upload</code> function handled the file selection, ensuring that only PDFs were accepted and imposing a reasonable file size limit of 10MB &#x2014; for now. </p><p>In the future, we could configure this based on the deployment server&apos;s available RAM.</p><h3 id="extracting-text-from-pdf">Extracting Text from PDF</h3><p>Once the user uploaded the PDF file, the <code>extract_text_from_pdf</code> function swung into action. Here&apos;s the code snippet for extracting text from the uploaded PDF:</p><pre><code class="language-python"># ... (rest of the code)

from pdfminer.high_level import extract_text_to_fp

def extract_text_from_pdf(pdf_file):
    pdf_buffer = BytesIO(pdf_file[&apos;content&apos;])
    text_buffer = StringIO()
    extract_text_to_fp(pdf_buffer, text_buffer)
    return text_buffer.getvalue()

# ... (rest of the code)
</code></pre><p>Here&apos;s where the magic happened. To extract text from the PDF, I utilized the <code>extract_text_to_fp</code> API from <code>pdfminer</code> to do the job. The concise <a href="https://pdfminersix.readthedocs.io/en/latest/tutorial/highlevel.html">PDFMiner documentation</a> guided me through the process.</p><h3 id="keeping-output-area-clean">Keeping Output Area Clean</h3><p>To maintain a clean user experience, I encapsulated the text output and download button in a &quot;scope&quot; using PyWebIO&apos;s <code>use_scope</code> function (<a href="https://pywebio.readthedocs.io/en/latest/guide.html#output-scope">reference</a>).</p><p>This allowed me to clear the specified output area every time a new file was uploaded, ensuring that each conversion was separate and didn&apos;t clutter the interface.</p><pre><code class="language-python"># ... (rest of the code)

def process_pdf():
    put_markdown(
        &quot;&quot;&quot;
        ## Convert PDF To Text
        &quot;&quot;&quot;,
    )

    while True:
        pdf_file = file_upload(
            &quot;Select PDF&quot;,
            accept=&quot;application/pdf&quot;,
            max_size=&quot;10M&quot;,
            multiple=False,
            help_text=&quot;sample.pdf&quot;,
        )
        clear(&apos;text-output-area&apos;) # NOTE: to clear previous text output

        text_output = extract_text_from_pdf(pdf_file)
        text_filename = f&quot;{Path(pdf_file[&apos;filename&apos;]).stem}.txt&quot;

        with use_scope(&apos;text-output-area&apos;):
            put_markdown(
                &quot;&quot;&quot;
                ### Text Output
                &quot;&quot;&quot;,
            )

            put_code(text_output, rows=10)
            put_buttons(
                [
                    &quot;Copy to Clipboard&quot;,
                    &apos;Click to Download&apos;,
                ],
                onclick=[
                    partial(
												copy_to_clipboard, # TODO: create later
												text=text_output
										),
                    partial(
                        click_to_download, # TODO: create later
                        filename=text_filename,
                        text=text_output.encode(),
                    ),
                ],
            )

# ... (rest of the code)
</code></pre><h3 id="download-text-output">Download Text Output</h3><p>Implementing a &#x201C;Click to Download&#x201D; feature for our app is super straightforward. In fact, you can choose between using <code>put_file</code> (<a href="https://pywebio.readthedocs.io/en/latest/output.html#pywebio.output.put_file">reference</a>) or <code>download</code> (<a href="https://pywebio.readthedocs.io/en/latest/session.html?highlight=download#pywebio.session.download">reference</a>).</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">See <a href="https://pywebio-demos.pywebio.online/doc_demo?app=demo-download">PyWebIO&#x2019;s demo</a> on download() usage.</div></div><p>I chose the latter because I wanted it to be a button instead of a link:</p><pre><code class="language-python"># ... (rest of the code)

def click_to_download(filename: str, text: bytes):
    download(
        filename,
        text,
    )
    toast(&apos;Text file downloaded&apos;)

# ... (rest of the code)
</code></pre><p>With this, users can download the output text with a simple click of a button.</p><h3 id="copy-to-clipboard">Copy to Clipboard</h3><p>Lastly, let&#x2019;s work on our &#x201C;Copy to Clipboard&#x201D; feature. This is where things are a little bit more tricky.</p><p>I learned that PyWebIO doesn&apos;t natively support this copy-to-clipboard function. Yet, the <a href="https://pywebio-demos.pywebio.online/doc_demo?app=demo-download">demo</a> above showcased its presence, sparking my determination to uncover its implementation.</p><p>Despite my thorough search through the official documentation, the feature remained elusive. However, my experience has led me to <a href="https://sourcegraph.com/github.com/pywebio/PyWebIO@e810dc45494348e27a5a4e1148985c619bed9a91/-/blob/demos/doc_demo.py?L100">Sourcegraph, where I unearthed the secrets behind its clever implementation</a>. Oh, the joy of discovery!</p><p>After taking inspiration from PyWebIO&apos;s demo example, here&#x2019;s what I arrived at:</p><pre><code class="language-python"># ... (rest of the code)

def copy_to_clipboard(text: str):
    clipboard_setup = &quot;&quot;&quot;
    window.writeText = function(text) {
        const input = document.createElement(&apos;textarea&apos;);
        input.style.opacity  = 0;
        input.style.position = &apos;absolute&apos;;
        input.style.left = &apos;-100000px&apos;;
        document.body.appendChild(input);

        input.value = text;
        input.select();
        input.setSelectionRange(0, text.length);
        document.execCommand(&apos;copy&apos;);
        document.body.removeChild(input);
        return true;
    }
    &quot;&quot;&quot;
    run_js(clipboard_setup)
    run_js(&quot;writeText(text)&quot;, text=text)
    toast(&apos;Text copied to the clipboard&apos;)

# ... (rest of the code)
</code></pre><p>When the user clicks the &quot;Copy to Clipboard&quot; button, the JavaScript code within <code>copy_to_clipboard</code> sets up a temporary <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea">textarea</a>, copies the text to it, selects the content, and executes the copy command.</p><p>As a result, the extracted text is now ready for pasting elsewhere, and a toast notification confirms the successful copy operation. Yay!</p><h2 id="full-code">Full Code</h2><p>You may find the full code example here:</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/ngshiheng/pypdf2txt/tree/v0.1.0"><div class="kg-bookmark-content"><div class="kg-bookmark-title">GitHub - ngshiheng/pypdf2txt at v0.1.0</div><div class="kg-bookmark-description">A simple Python web service that allows you to convert your PDF documents to text. - GitHub - ngshiheng/pypdf2txt at v0.1.0</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.com/fluidicon.png" alt="Build Your Own: Python PDF to Text"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">ngshiheng</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/c8bafb2f127532a6ede369ea635c6a81b8022bced96c633859b97058c597bc9b/ngshiheng/pypdf2txt" alt="Build Your Own: Python PDF to Text"></div></a></figure><h2 id="closing-thoughts">Closing Thoughts</h2><p>Congratulations! You now have the power of converting PDF to text at your fingertips, without sacrificing privacy, security, and ownership.</p><p>I remember the first time I stumbled upon PyWebIO, and I was immediately intrigued by its promise of creating web apps with just a few lines of Python code. While it might not suit complex projects, for simple tools, it&apos;s amazing.</p><p>If I were to build a GUI application, I would have gone for something like <a href="https://wiki.python.org/moin/PyQt">PyQt</a>. Today, I&#x2019;d say no more GUI frameworks; just one browser app is all you need.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Looking for alternatives? Check out <a href="https://streamlit.io/">Streamlit</a> and <a href="https://anvil.works/">Avril</a>.</div></div><p>Happy building!</p>]]></content:encoded></item><item><title><![CDATA[Go Module Proxy at Grab]]></title><description><![CDATA[<div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-text">Originally published on the <a href="https://engineering.grab.com/">Grab Tech Blog</a>. Read the <a href="https://engineering.grab.com/go-module-proxy">original post</a> on engineering.grab.com.</div></div><p>At Grab, we rely heavily on <a href="https://engineering.grab.com/go-module-a-guide-for-monorepos-part-1">a large Go monorepo</a> for backend development, which offers benefits like code reusability and discoverability. However, as we continue to grow, managing a large monorepo brings about its own</p>]]></description><link>https://jerrynsh.com/go-module-proxy-at-grab/</link><guid isPermaLink="false">64aa223f42049b95d662466f</guid><category><![CDATA[Go]]></category><dc:creator><![CDATA[Jerry Ng]]></dc:creator><pubDate>Tue, 11 Jul 2023 00:00:37 GMT</pubDate><media:content url="https://jerrynsh.com/content/images/2023/07/go-module-proxy-at-grab-1.png" medium="image"/><content:encoded><![CDATA[<div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-text">Originally published on the <a href="https://engineering.grab.com/">Grab Tech Blog</a>. Read the <a href="https://engineering.grab.com/go-module-proxy">original post</a> on engineering.grab.com.</div></div><img src="https://jerrynsh.com/content/images/2023/07/go-module-proxy-at-grab-1.png" alt="Go Module Proxy at Grab"><p>At Grab, we rely heavily on <a href="https://engineering.grab.com/go-module-a-guide-for-monorepos-part-1">a large Go monorepo</a> for backend development, which offers benefits like code reusability and discoverability. However, as we continue to grow, managing a large monorepo brings about its own set of unique challenges.</p><p>As an example, using Go commands such as <code>go get</code> and <code>go list</code> can be incredibly slow when fetching Go modules residing in a large <a href="https://github.com/golang/go/wiki/Modules#what-are-multi-module-repositories">multi-module repository</a>. This sluggishness takes a toll on developer productivity, burdens our Continuous Integration (CI) systems, and strains our Version Control System host (VCS), GitLab.</p><p>In this blog post, we look at how <a href="https://github.com/gomods/athens">Athens</a>, a Go module proxy, helps to improve the overall developer experience of engineers working with a large Go monorepo at Grab.</p><h3 id="key-highlights">Key highlights</h3><ul><li>We reduced the time of executing the <code>go get</code> command from <strong>~18 minutes</strong> to <strong>~12 seconds</strong> when fetching monorepo Go modules.</li><li>We scaled in and <strong>scaled down our entire Athens cluster by 70%</strong> by utilising the fallback network mode in Athens along with Golang&#x2019;s <code>GOVCS</code> mode, resulting in cost savings and enhanced efficiency.</li></ul><h2 id="problem-statements-and-solutions">Problem statements and solutions</h2><h3 id="1-painfully-slow-performance-of-go-commands">1. Painfully slow performance of Go commands</h3><blockquote><em>Problem summary: Running the <code>go get</code> command in our monorepo takes a considerable amount of time and can lead to performance degradation in our VCS.</em></blockquote><p>When working with the Go programming language, <code>go get</code> is one of the most common commands that you&#x2019;ll use every day. Besides developers, this command is also used by CI systems.</p><h4 id="what-does-go-get-do">What does go get do?</h4><p>The <code>go get</code> command is used to download and install packages and their dependencies in Go. Note that it operates differently depending on whether it is run in <a href="https://pkg.go.dev/cmd/go#hdr-Legacy_GOPATH_go_get">legacy GOPATH mode</a> or module-aware mode. In Grab, we&#x2019;re using the <a href="https://go.dev/ref/mod#mod-commands">module-aware mode</a> in a <a href="https://github.com/golang/go/wiki/Modules#faqs--multi-module-repositories">multi-module repository</a> setup.</p><figure class="kg-card kg-image-card"><img src="https://jerrynsh.com/content/images/2023/07/image.png" class="kg-image" alt="Go Module Proxy at Grab" loading="lazy" width="663" height="263" srcset="https://jerrynsh.com/content/images/size/w600/2023/07/image.png 600w, https://jerrynsh.com/content/images/2023/07/image.png 663w"></figure><p>Every time <code>go get</code> is run, it uses Git commands, like <code>git ls-remote</code>, <code>git tag</code>, <code>git fetch</code>, etc, to search and download the entire worktree. The excessive use of these Git commands on our monorepo contributes to the long processing time and can be strenuous to our VCS.</p><h4 id="how-big-is-our-monorepo">How big is our monorepo?</h4><p>To fully grasp the challenges faced by our engineering teams, it&#x2019;s crucial to understand the vast scale of the monorepo that we work with daily. For this, we use <a href="https://github.com/github/git-sizer">git-sizer</a> to analyse our monorepo.</p><p>Here&#x2019;s what we found:</p><ul><li><strong>Overall repository size</strong>: The monorepo has a total uncompressed size of <strong>69.3 GiB</strong>, a fairly substantial figure. To put things into perspective, the <a href="https://github.com/torvalds/linux">Linux kernel repository</a>, known for its vastness, currently stands at 55.8 GiB.</li><li><strong>Trees</strong>: The total number of trees is 3.21M and tree entries are 99.8M, which consume 3.65 GiB. This may cause performance issues during some Git operations.</li><li><strong>References</strong>: Totalling 10.7k references.</li><li><strong>Biggest checkouts</strong>: There are 64.7k directories in our monorepo. This affects operations like <code>git status</code> and <code>git checkout</code>. Moreover, our monorepo has a maximum path depth of 20. This contributes to a slow processing time on Git and negatively impacts developer experience. The number of files (354k) and the total size of files (5.08 GiB) are also concerns due to their potential impact on the repository&#x2019;s performance.</li></ul><p>To draw a comparison, refer to <a href="https://github.com/github/git-sizer/blob/0b6d3a21c6ccbd49463534a19cc1b3f71526c077/README.md#usage">the <code>git-sizer</code> output of the Linux repository</a>.</p><h4 id="how-slow-is-%E2%80%9Cslow%E2%80%9D">How slow is &#x201C;slow&#x201D;?</h4><p>To illustrate the issue further, we will compare the time taken for various Go commands to fetch a single module in our monorepo at a 10 MBps download speed.</p><p>This is an example of how a module is structured in our monorepo:</p><pre><code class="language-shell">gitlab.company.com/monorepo/go
&#xA0; |--&#xA0;go.mod
&#xA0; |--&#xA0;commons/util/gk
&#xA0; &#xA0; &#xA0; &#xA0; |--&#xA0;go.mod

</code></pre><table>
<thead>
<tr>
<th>Go Commands</th>
<th>GOPROXY</th>
<th>Previously Cached?</th>
<th>Description</th>
<th>Result (time taken)</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>go get -x gitlab.company.com/monorepo/go/commons/util/gk</code></td>
<td>proxy.golang.org,direct</td>
<td>Yes</td>
<td>Download and install the latest version of the module. This is a common scenario that developers often encounter.</td>
<td>18:50.71 minutes</td>
</tr>
<tr>
<td><code>go get -x gitlab.company.com/monorepo/go/commons/util/gk</code></td>
<td>proxy.golang.org,direct</td>
<td>No</td>
<td>Download and install the latest version of the module without any module cache</td>
<td>1:11:54.56 hour</td>
</tr>
<tr>
<td><code>go list -x -m -json -versions gitlab.company.com/monorepo/go/util/gk</code></td>
<td>proxy.golang.org,direct</td>
<td>Yes</td>
<td>List information about the module</td>
<td>3.873 seconds</td>
</tr>
<tr>
<td><code>go list -x -m -json -versions gitlab.company.com/monorepo/go/util/gk</code></td>
<td>proxy.golang.org,direct</td>
<td>No</td>
<td>List information about the module without any module cache</td>
<td>3:18.58 minutes</td>
</tr>
</tbody>
</table>
<p>In this example, using <code>go get</code> to fetch a module took over <strong>18 minutes</strong> to complete. If we needed to retrieve more than one module in our monorepo, it can be incredibly time-consuming.</p><h4 id="why-is-it-slow-in-a-monorepo">Why is it slow in a monorepo?</h4><p>In a large Go monorepo, <code>go get</code> commands can be slow due to several factors:</p><ol><li><strong>Large number of files and directories</strong>: When running <code>go get</code>, the command needs to search and download the entire worktree. In a large multi-module monorepo, the vast number of files and directories make this search process very expensive and time-consuming.</li><li><strong>Number of refs</strong>: A large number of refs (branches or tags) in our monorepo can affect performance. Ref advertisements (<code>git ls-remote</code>), which contain every ref in our monorepo, are the first phase in any remote Git operation, such as <code>git clone</code> or <code>git fetch</code>. With a large number of refs, performance takes a hit when performing these operations.</li><li><strong>Commit history traversal</strong>: Operations that need to traverse a repository&#x2019;s commit history and consider each ref will be slow in a monorepo. The larger the monorepo, the more time-consuming these operations become.</li></ol><h4 id="the-consequences-stifled-productivity-and-strained-systems">The consequences: Stifled productivity and strained systems</h4><h5 id="developers-and-ci">Developers and CI</h5><p>When Go command operations like <code>go get</code> are slow, they contribute to significant delays and inefficiencies in software development workflows. This leads to reduced productivity and demotivated developers.</p><p>Optimising Go command operations&#x2019; speed is crucial to ensure efficient software development workflows and high-quality software products.</p><h5 id="version-control-system">Version Control System</h5><p>It&#x2019;s also worth noting that overusing <code>go get</code> commands can also lead to performance issues for VCS. When Go packages are frequently downloaded using <code>go get</code>, we saw that it caused a bottleneck in our VCS cluster, which can lead to performance degradation or even cause rate-limiting queue issues.</p><p>This negatively impacts the performance of our VCS infrastructure, causing delays or sometimes unavailability for some users and CI.</p><h4 id="solution-athens-fallback-network-mode-govcs-custom-cache-refresh-solution">Solution: Athens + fallback Network Mode + GOVCS + Custom Cache Refresh Solution</h4><blockquote><em>Problem summary: Speed up <code>go get</code> command by not fetching from our VCS</em></blockquote><p>We addressed the speed issue by using Athens, <a href="https://www.practical-go-lessons.com/chap-18-go-module-proxies#what-is-a-proxy-server">a proxy server for Go modules</a> (read more about the <a href="https://go.dev/ref/mod#goproxy-protocol">GOPROXY protocol</a>).</p><h5 id="how-does-athens-work">How does Athens work?</h5><p>The following sequence diagram describes the default flow of <code>go get</code> command with Athens.</p><figure class="kg-card kg-image-card"><img src="https://jerrynsh.com/content/images/2023/07/image-1.png" class="kg-image" alt="Go Module Proxy at Grab" loading="lazy" width="784" height="315" srcset="https://jerrynsh.com/content/images/size/w600/2023/07/image-1.png 600w, https://jerrynsh.com/content/images/2023/07/image-1.png 784w" sizes="(min-width: 720px) 720px"></figure><p>Athens uses a <a href="https://docs.gomods.io/configuration/storage/">storage system</a> for Go module packages, which can also be configured to use various storage systems such as Amazon S3, and Google Cloud Storage, among others.</p><p>By caching these module packages in storage, Athens can serve the packages directly from storage rather than requesting them from an upstream VCS while serving Go commands such as <code>go mod download</code> and <a href="https://go.dev/ref/mod#build-commands">certain go build modes</a>. However, just using a Go module proxy didn&#x2019;t fully resolve our issue since the <strong><code>go get</code> and <code>go list</code></strong> commands still hit our VCS through the proxy.</p><p>With this in mind, we thought &#x201C;what if we could just serve the Go modules directly from Athens&#x2019; storage for <code>go get</code>?&#x201D; This question led us to discover Athens network mode.</p><h5 id="what-is-athens-network-mode">What is Athens network mode?</h5><p>Athens <code>NetworkMode</code> configures how Athens will return the results of the Go commands. It can be assembled from both its own storage and the upstream VCS. As of <a href="https://github.com/gomods/athens/releases/tag/v0.12.1">Athens v0.12.1</a>, it currently supports these 3 modes:</p><ol><li><strong>strict</strong>: merge VCS versions with storage versions, but fail if either of them fails.</li><li><strong>offline</strong>: only get storage versions, <strong>never reach out to VCS</strong>.</li><li><strong>fallback</strong>: only return storage versions, if VCS fails. Fallback mode does the best effort of giving you what&#x2019;s available at the time of requesting versions.</li></ol><p>Our Athens clusters were initially set to use <code>strict</code> network mode, but this was not ideal for us. So we explored the other network modes.</p><h5 id="exploring-offline-mode">Exploring offline mode</h5><p>We initially sought to explore the idea of putting Athens in <code>offline</code> network mode, which would allow Athens to serve Go requests only from its storage. This concept aligned with our aim of reducing VCS hits while also leading to significant performance improvement in Go workflows.</p><figure class="kg-card kg-image-card"><img src="https://jerrynsh.com/content/images/2023/07/image-2.png" class="kg-image" alt="Go Module Proxy at Grab" loading="lazy" width="784" height="308" srcset="https://jerrynsh.com/content/images/size/w600/2023/07/image-2.png 600w, https://jerrynsh.com/content/images/2023/07/image-2.png 784w" sizes="(min-width: 720px) 720px"></figure><p>However in practice, it&#x2019;s not an ideal approach. The default Athens setup (<code>strict</code> mode) automatically updates the module version when a user requests a new module version. Nevertheless, switching Athens to <code>offline</code> mode would disable the automatic updates as it wouldn&#x2019;t connect to the VCS.</p><h5 id="custom-cache-refresh-solution">Custom cache refresh solution</h5><p>To solve this, we implemented a CI pipeline that refreshes Athens&#x2019; module cache whenever a new module is released in our monorepo. Employing this with <code>offline</code> mode made Athens effective for the monorepo but it resulted in the loss of automatic updates for other repositories</p><p>Restoring this feature requires applying our custom cache refresh solution to all other Go repositories. However, implementing this workaround can be quite cumbersome and significant additional time and effort. We decided to look for another solution that would be easier to maintain in the long run.</p><h5 id="a-balanced-approach-fallback-mode-and-govcs">A balanced approach: fallback Mode and GOVCS</h5><p>This approach builds upon our aforementioned custom cache refresh which is specifically designed for the monorepo.</p><p>We came across the <a href="https://go.dev/ref/mod#vcs-govcs">GOVCS environment variable</a>, which we use in combination with the <code>fallback</code> network mode to effectively put only the monorepo in &#x201C;offline&#x201D; mode.</p><p>When <code>GOVCS</code> is set to <code>gitlab.company.com/monorepo/go:off</code>, Athens encounters an error whenever it tries to fetch modules from VCS:</p><pre><code>gitlab.company.com/monorepo/go/commons/util/gk@v1.1.44:&#xA0;unrecognized&#xA0;import&#xA0;path&#xA0;&quot;gitlab.company.com/monorepo/go/commons/util/gk&quot;:&#xA0;GOVCS&#xA0;disallows&#xA0;using&#xA0;git&#xA0;for&#xA0;private&#xA0;gitlab.company.com/monorepo/go;&#xA0;see&#xA0;&apos;go&#xA0;help&#xA0;vcs&apos;

</code></pre><p>If Athens network mode is set to <code>strict</code>, Athens returns 404 errors to the user. By switching to <code>fallback</code> mode, Athens tries to retrieve the module from its storage if a <code>GOVCS</code> failure occurs.</p><p>Here&#x2019;s the updated Athens configuration (<a href="https://github.com/gomods/athens/blob/8e1581e10b0d3a70a30f45b10c24c3f992464d7a/config.dev.toml#L46">example default config</a>):</p><pre><code>GoBinaryEnvVars&#xA0;=&#xA0;[&quot;GOPROXY=direct&quot;,
&quot;GOPRIVATE=gitlab.company.com&quot;,
&quot;GOVCS=gitlab.company.com/monorepo/go:off&quot;]

NetworkMode&#xA0;=&#xA0;&quot;fallback&quot;

</code></pre><p>With the custom cache refresh solution coupled with this approach, we not only accelerate the retrieval of Go modules within the monorepo but also allow for automatic updates for non-monorepo Go modules.</p><h4 id="final-results">Final results</h4><p>This solution resulted in a significant improvement in the performance of Go commands for our developers. With Athens, the same command is completed in just <strong>~12 seconds (down from ~18 minutes)</strong>, which is impressively fast.</p><table>
<thead>
<tr>
<th>Go Commands</th>
<th>GOPROXY</th>
<th>Previously Cached?</th>
<th>Description</th>
<th>Result (time taken)</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>go get -x gitlab.company.com/monorepo/go/commons/util/gk</code></td>
<td>goproxy.company.com</td>
<td>Yes</td>
<td>Download and install the latest version of the module. This is a common scenario that developers often encounter.</td>
<td>11.556 seconds</td>
</tr>
<tr>
<td><code>go get -x gitlab.company.com/monorepo/go/commons/util/gk</code></td>
<td>goproxy.company.com</td>
<td>No</td>
<td>Download and install the latest version of the module without any module cache</td>
<td>1:05.60 minutes</td>
</tr>
<tr>
<td><code>go list -x -m -json -versions gitlab.company.com/monorepo/go/util/gk</code></td>
<td>goproxy.company.com</td>
<td>Yes</td>
<td>List information about the monorepo module</td>
<td>0.592 seconds</td>
</tr>
<tr>
<td><code>go list -x -m -json -versions gitlab.company.com/monorepo/go/util/gk</code></td>
<td>goproxy.company.com</td>
<td>No</td>
<td>List information about the monorepo module without any module cache</td>
<td>1.023 seconds</td>
</tr>
</tbody>
</table>
<figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/07/image-3.png" class="kg-image" alt="Go Module Proxy at Grab" loading="lazy" width="1284" height="866" srcset="https://jerrynsh.com/content/images/size/w600/2023/07/image-3.png 600w, https://jerrynsh.com/content/images/size/w1000/2023/07/image-3.png 1000w, https://jerrynsh.com/content/images/2023/07/image-3.png 1284w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Average cluster CPU utlisation</span></figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/07/image-4.png" class="kg-image" alt="Go Module Proxy at Grab" loading="lazy" width="1284" height="866" srcset="https://jerrynsh.com/content/images/size/w600/2023/07/image-4.png 600w, https://jerrynsh.com/content/images/size/w1000/2023/07/image-4.png 1000w, https://jerrynsh.com/content/images/2023/07/image-4.png 1284w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Average cluster memory utlisation</span></figcaption></figure><p>In addition, this change to our Athens cluster also leads to substantial reduction in average cluster CPU and memory utilisation. This also enabled us to scale in and <strong>scale down our entire Athens cluster by 70%</strong>, resulting in cost savings and enhanced efficiency. On top of that, we were also able to effectively eliminate VCS&#x2019;s rate-limiting issues while making the monorepo&#x2019;s command operation considerably faster.</p><h3 id="2-go-modules-in-gitlab-subgroups">2. Go modules in GitLab subgroups</h3><blockquote><em>Problem summary: Go modules are unable to work natively with private or internal repositories under GitLab subgroups.</em></blockquote><p>When it comes to managing code repositories and packages, <a href="https://docs.gitlab.com/ee/user/group/subgroups/">GitLab subgroups</a> and Go modules have become an integral part of the development process at Grab. Go modules help to organise and manage dependencies, and GitLab subgroups provide an additional layer of structure to group related repositories together.</p><p>However, a common issue when using Go modules is that they <strong>do not work natively</strong> with private or internal repositories under a GitLab subgroup (see this <a href="https://github.com/golang/go/issues/29953">GitHub issue</a>).</p><p>For example, using <code>go get</code> to retrieve a module from <code>gitlab.company.com/gitlab-org/subgroup/repo</code> will result in a failure. This problem is not specific to Go modules, all repositories under the subgroup will face the same issue.</p><h4 id="a-cumbersome-workaround">A cumbersome workaround</h4><p>To overcome this issue, we had to use workarounds. One workaround is to authenticate the HTTPS calls to GitLab by adding authentication details to the <code>.netrc</code> file on your machine.</p><p>The following lines can be added to the <code>.netrc</code> file:</p><pre><code class="language-.netrc">machine&#xA0;gitlab.company.com
    login&#xA0;user@company.com
    password&#xA0;&lt;personal-access-token&gt;</code></pre><p>In our case, we are using a Personal Access Token (PAT) since we have 2FA enabled. If 2FA is not enabled, the GitLab password can be used instead. However, this approach would mean configuring the <code>.netrc</code> file in the CI environments as well as on the machine of <strong>every</strong> Go developer.</p><h4 id="solution-athens-netrc">Solution: Athens + .netrc</h4><p>A feasible solution is to set up the <code>.netrc</code> file in the Go proxy server. This method eliminates the need for N number of developers to configure their own <code>.netrc</code> files. Instead, the responsibility for this task is delegated to the Go proxy server.</p><h3 id="3-sharing-common-libraries">3. Sharing common libraries</h3><blockquote><em>Problem summary: Distributing internal common libraries within a monorepo without granting direct repository access can be challenging.</em></blockquote><p>At Grab, we work with various cross-functional teams, and some could have distinct network access like different VPNs. This adds complexity to sharing our monorepo&#x2019;s internal common libraries with them. To maintain the security and integrity of our monorepo, we use a Go proxy for controlled access to necessary libraries.</p><p>The key difference between granting direct access to the monorepo via VCS and using a Go proxy is that the former allows users to read everything in the repository, while the latter enables us to grant access only to the specific libraries users need within the monorepo. This approach ensures secure and efficient collaboration across diverse network configurations.</p><h4 id="without-go-module-proxy">Without Go module proxy</h4><p>Without Athens, we would need to create a separate repository to store the code we want to share and then use a build system to automatically mirror the code from the monorepo to the public repository.</p><p>This process can be cumbersome and lead to inconsistencies in code versions between the two repositories, ultimately making it challenging to maintain the shared libraries.</p><p>Furthermore, copying code can lead to errors and increase the risk of security breaches by exposing confidential or sensitive information.</p><h4 id="solution-athens-download-mode-file">Solution: Athens + Download Mode File</h4><p>To tackle this problem statement, we utilise Athens&#x2019; <a href="https://docs.gomods.io/configuration/download/">download mode file</a> feature using an allowlist approach to specify which repositories can be downloaded by users.</p><p>Here&#x2019;s an example of the Athens download mode config file:</p><pre><code class="language-hcl">downloadURL&#xA0;=&#xA0;&quot;https://proxy.golang.org&quot;

mode&#xA0;=&#xA0;&quot;sync&quot;

download&#xA0;&quot;gitlab.company.com/repo/a&quot;&#xA0;{
&#xA0; &#xA0; mode&#xA0;=&#xA0;&quot;sync&quot;
}

download&#xA0;&quot;gitlab.company.com/repo/b&quot;&#xA0;{
&#xA0; &#xA0; mode&#xA0;=&#xA0;&quot;sync&quot;
}

download&#xA0;&quot;gitlab.company.com/*&quot;&#xA0;{
&#xA0; &#xA0; mode&#xA0;=&#xA0;&quot;none&quot;
}</code></pre><p>In the configuration file, we specify allowlist entries for each desired repo, including their respective download modes. For example, in the snippet above, <code>repo/a</code> and <code>repo/b</code> are allowed (<code>mode = &#x201C;sync&#x201D;</code>), while everything else is blocked using <code>mode = &#x201C;none&#x201D;</code>.</p><h4 id="final-results-1">Final results</h4><p>By using Athens&#x2019; download mode feature in this case, the benefits are clear. Athens provides a secure, centralised place to store Go modules. This approach not only provides consistency but also improves maintainability, as all code versions are managed in one single location.</p><h2 id="additional-benefits-of-go-proxy">Additional benefits of Go proxy</h2><p>As we&#x2019;ve touched upon the impressive results achieved by implementing Athens Go proxy at Grab, it&#x2019;s crucial to explore the supplementary advantages that accompany this powerful solution.</p><p>These unsung benefits, though possibly overlooked, play a vital role in enriching the overall developer experience at Grab and promoting more robust software development practices:</p><ol><li><strong>Module immutability</strong>: As the software world continues to face issues around changing or <a href="https://qz.com/646467/how-one-programmer-broke-the-internet-by-deleting-a-tiny-piece-of-code">disappearing libraries</a>, Athens serves as a useful tool in mitigating build disruptions by providing immutable storage for copied VCS code. The use of a Go proxy also ensures that builds remain deterministic, improving consistency across our software.</li><li><strong>Uninterrupted development</strong>: Athens allows users to fetch dependencies even when VCS is down, ensuring continuous and seamless development workflows.</li><li><strong>Enhanced security</strong>: Athens offers access control by enabling the blocking of specific packages within Grab. This added layer of security protects our work against potential risks from malicious third-party packages.</li><li><strong>Vendor directory removal</strong>: Athens prepares us for the eventual removal of the <a href="https://docs.gomods.io/faq/#when-should-i-use-a-vendor-directory-and-when-should-i-use-athens">vendor directory</a>, fostering faster workflows in the future.</li></ol><h2 id="what%E2%80%99s-next">What&#x2019;s next?</h2><p>Since adopting Athens as a Go module proxy, we have observed considerable benefits, such as:</p><ol><li>Accelerated Go command operations</li><li>Reduced infrastructure costs</li><li>Mitigated VCS load issues</li></ol><p>Moreover, its lesser-known advantages like module immutability, uninterrupted development, enhanced security, and vendor directory transition have also contributed to improved development practices and an enriched developer experience for Grab engineers.</p><p>Today, the straightforward process of exporting three environment variables has greatly influenced our developers&#x2019; experience at Grab.</p><pre><code class="language-bash">export&#xA0;GOPROXY=&quot;goproxy.company.com|proxy.golang.org,direct&quot;
export&#xA0;GONOSUMDB=&quot;gitlab.company.com&quot;
export&#xA0;GONOPROXY=&quot;none&quot;</code></pre><p>At Grab, we are always looking for ways to improve and optimise the way we work, so we contribute to open-sourced projects like Athens, where we help with bug fixes. If you are interested in setting up a Go module proxy, do give Athens (<a href="https://github.com/gomods/athens">github.com/gomods/athens</a>) a try!</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-text"><i><em class="italic" style="white-space: pre-wrap;">Special thanks to Swaminathan Venkatraman, En Wei Soh, Anuj More, Darius Tan, and Fernando Christyanto for contributing to this project and this article.</em></i></div></div>]]></content:encoded></item><item><title><![CDATA[How To Write Testable Code in Python]]></title><description><![CDATA[Discover how separating I/O operations from core logic in Python can improve code testability. Briefly explore the concept of dependency injection]]></description><link>https://jerrynsh.com/how-to-write-testable-code-in-python/</link><guid isPermaLink="false">649d2758ea150ac2c515d4c2</guid><category><![CDATA[Python]]></category><dc:creator><![CDATA[Jerry Ng]]></dc:creator><pubDate>Fri, 30 Jun 2023 00:00:27 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1600267147646-33cf514b5f3e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDd8fHRlc3Rpbmd8ZW58MHx8fHwxNjg4MDIwODI3fDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1600267147646-33cf514b5f3e?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8c2VhcmNofDd8fHRlc3Rpbmd8ZW58MHx8fHwxNjg4MDIwODI3fDA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="How To Write Testable Code in Python"><p>So, a few weeks back, I stumbled upon <a href="https://www.youtube.com/watch?v=DJtef410XaM&amp;t=385s&amp;pp=ygUZY2xlYW4gYXJjaGl0ZWN0dXJlIHB5dGhvbg%3D%3D">this awesome talk by Brandon Rhodes</a>. I was so hooked that I couldn&apos;t resist the temptation to dive right into the example he shared and jot down my learnings.</p><p>One key takeaway I&apos;ve learned is the importance of separating I/O operations (i.e. network requests, database calls, etc.) from the core logic of our code. By doing so, we can make our code more modular and testable.</p><p>I won&apos;t be delving into the intricacies of <a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html">Clean Architecture</a> or <a href="https://gist.github.com/wojteklu/73c6914cc446146b8b533c0988cf8d29">Clean Code</a> in this blog post. While those concepts hold their own value, I want to focus on something that you can put into use immediately.</p><p>Let&apos;s dive right into an example that will serve as our starting point. This example is taken directly from <a href="https://twitter.com/brandon_rhodes">Brandon Rhodes</a>&apos; slides, which you can find <a href="https://rhodesmill.org/brandon/slides/2014-07-pyohio/clean-architecture/">here</a>.</p><h2 id="exploring-the-finddefinition-function">Exploring the <code>find_definition</code> Function</h2><p>Imagine we have a Python function called <code>find_definition</code> that performs data processing and involves making HTTP requests to an external API.</p><pre><code class="language-python">import requests                      # Listing 1
from urllib.parse import urlencode

def find_definition(word):
    q = &apos;define &apos; + word
    url = &apos;http://api.duckduckgo.com/?&apos;
    url += urlencode({&apos;q&apos;: q, &apos;format&apos;: &apos;json&apos;})
    response = requests.get(url)     # I/O
    data = response.json()           # I/O
    definition = data[u&apos;Definition&apos;]
    if definition == u&apos;&apos;:
        raise ValueError(&apos;that is not a word&apos;)
    return definition
</code></pre><h3 id="writing-our-first-test">Writing our first test</h3><p>To write a unit test for the <code>find_definition</code> function, we can utilize <a href="https://docs.python.org/3/library/unittest.html">Python&apos;s built-in <code>unittest</code> module</a>. Here&apos;s an example of how we can approach it:</p><pre><code class="language-python">import unittest
from unittest.mock import patch

class TestFindDefinition(unittest.TestCase):
    @patch(&apos;requests.get&apos;)
    def test_find_definition(self, mock_get):
        mock_response = {u&apos;Definition&apos;: &apos;Visit tournacat.com&apos;}
        mock_get.return_value.json.return_value = mock_response
        
        expected_definition = &apos;Visit tournacat.com&apos;
        definition = find_definition(&apos;tournacat&apos;)
        
        self.assertEqual(definition, expected_definition)
        mock_get.assert_called_with(&apos;http://api.duckduckgo.com/?q=define+tournacat&amp;format=json&apos;)
</code></pre><p>To isolate the I/O operations, we use the <code>patch</code> decorator from the <code>unittest.mock</code> module. It allows us to <a href="https://stackoverflow.com/a/2666006"><em>mock</em></a> the behavior of the <code>requests.get</code> function.</p><p>By doing so, we can control the response that our function receives during testing. This way, we can test the <code>find_definition</code> function in isolation without actually making real HTTP requests.</p><h3 id="testing-difficulties-and-tight-coupling">Testing difficulties and tight coupling</h3><p>By using the <code>patch</code> decorator to mock the behavior of the <code>requests.get</code> function, we tightly couple the tests to the internal workings of the function. This makes the tests more susceptible to breaking if there are changes in the implementation or dependencies.</p><p>If the implementation of <code>find_definition</code> changes, such as:</p><ol><li>Using a different HTTP library</li><li>Modifying the structure of the API response</li><li>Changes in the API endpoint</li></ol><p>The tests may need to be updated accordingly. In the case of <code>find_definition</code>, writing and maintaining unit tests becomes a cumbersome task.</p><h2 id="hiding-io-a-common-mistake">Hiding I/O: A Common Mistake</h2><p>Typically, when working with functions like <code>find_definition</code> that involve I/O operations, I&#x2019;d often refactor the code to extract the I/O operations into a separate function, such as <code>call_json_api</code>, as shown in the updated code below (again, borrowed from <a href="https://rhodesmill.org/brandon/slides/2014-07-pyohio/clean-architecture/">Brandon&#x2019;s slides</a>):</p><pre><code class="language-python">def find_definition(word):           # Listing 2
    q = &apos;define &apos; + word
    url = &apos;http://api.duckduckgo.com/?&apos;
    url += urlencode({&apos;q&apos;: q, &apos;format&apos;: &apos;json&apos;})
    data = call_json_api(url)
    definition = data[u&apos;Definition&apos;]
    if definition == u&apos;&apos;:
        raise ValueError(&apos;that is not a word&apos;)
    return definition

def call_json_api(url):
    response = requests.get(url)     # I/O
    data = response.json()           # I/O
    return data
</code></pre><p>By extracting the I/O operations into a separate function, we achieve a level of abstraction and encapsulation. </p><p>The <code>find_definition</code> function now delegates the responsibility of making the HTTP request and parsing the JSON response to the <code>call_json_api</code> function.</p><h3 id="updating-the-test">Updating the test</h3><p>Again, we utilize the <code>patch</code> decorator from the <code>unittest.mock</code> module to mock the behavior of the <code>call_json_api</code> function (instead of <code>requests.get</code>). By doing so, we can control the response that <code>find_definition</code> receives during testing.</p><pre><code class="language-python">import unittest
from unittest.mock import patch

class TestFindDefinition(unittest.TestCase):
    @patch(&apos;call_json_api&apos;)
    def test_find_definition(self, mock_call_json_api):
        mock_response = {u&apos;Definition&apos;: &apos;Visit tournacat.com&apos;}
        mock_call_json_api.return_value = mock_response
        
        expected_definition = &apos;Visit tournacat.com&apos;
        definition = find_definition(&apos;tournacat&apos;)
        
        self.assertEqual(definition, expected_definition)
        mock_call_json_api.assert_called_with(&apos;http://api.duckduckgo.com/?q=define+tournacat&amp;format=json&apos;)
</code></pre><h3 id="%E2%80%9Dwe-have-hidden-io-but-have-we-really-decoupled-it%E2%80%9D">&#x201D;We have hidden I/O, but have we <em>really</em> decoupled it?&#x201D;</h3><p>However, it&apos;s important to note that although we have hidden the I/O operations behind the <code>call_json_api</code> function, we haven&apos;t completely decoupled them. </p><p>The <code>find_definition</code> function <em>still</em> depends on the implementation details &#xA0;<code>call_json_api</code> and assumes it will handle the I/O operations correctly.</p><h2 id="dependency-injection-decoupling">Dependency Injection: Decoupling</h2><p>To achieve a more decoupled design, we could further separate the I/O concerns by using dependency injection.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Read <a href="https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/">A quick intro to Dependency Injection</a>.</div></div><p>Here&apos;s an updated version of the <code>find_definition</code>:</p><pre><code class="language-python">import requests

def find_definition(word, api_client=requests):  # Dependency injection
    q = &apos;define &apos; + word
    url = &apos;http://api.duckduckgo.com/?&apos;
    url += urlencode({&apos;q&apos;: q, &apos;format&apos;: &apos;json&apos;})
    response = api_client.get(url)               # I/O 
    data = response.json()                       # I/O
    definition = data[u&apos;Definition&apos;]
    if definition == u&apos;&apos;:
        raise ValueError(&apos;that is not a word&apos;)
    return definition
</code></pre><p>The <code>api_client</code> parameter is introduced, which represents the dependency responsible for making the API calls. By default, it is set to <code>requests</code>, allowing us to use the <code>requests</code> library for the I/O operations.</p><h3 id="unit-testing-with-dependency-injection">Unit testing with dependency injection</h3><p>Using dependency injection allows for better control and predictability in unit testing. Here&apos;s an example of how we can write unit tests for the <code>find_definition</code> function with dependency injection:</p><pre><code class="language-python">import unittest
from unittest.mock import MagicMock

class TestFindDefinition(unittest.TestCase):
    def test_find_definition(self):
        mock_response = {u&apos;Definition&apos;: u&apos;How to add Esports schedules to Google Calendar?&apos;}
        mock_api_client = MagicMock()
        mock_api_client.get.return_value.json.return_value = mock_response

        word = &apos;example&apos;
        expected_definition = &apos;How to add Esports schedules to Google Calendar?&apos;

        definition = find_definition(word, api_client=mock_api_client)

        self.assertEqual(definition, expected_definition)
        mock_api_client.get.assert_called_once_with(&apos;http://api.duckduckgo.com/?q=define+example&amp;format=json&apos;)
</code></pre><p>In the updated unit test example, we create a mock API client using the <code>MagicMock</code> class from the <code>unittest.mock</code> module. The mock API client is configured to return a predefined response i.e. <code>mock_response</code> when its <code>get</code> method is called. </p><p>Yay! In the case where we want to use a different HTTP library, we&apos;re now in a much better spot.</p><h3 id="problems-with-dependency-injection">Problems with dependency injection</h3><p>While dependency injection offers several benefits, it can also introduce some challenges. As highlighted by Brandon, there are a few potential problems to be aware of:</p><ol><li><strong>Mock vs. Real Library</strong>: The mock objects used for testing might not fully replicate the behavior of the real dependencies. This could lead to discrepancies between test results and actual runtime behavior.</li><li><strong>Complex Dependencies</strong>: Functions or components with multiple dependencies, such as a combination of database, filesystem, and external services, can require significant injection setup and management, making the codebase more complex.</li></ol><p>This brings us to the next point.</p><h2 id="separating-io-operations-from-core-logic">Separating I/O Operations from Core Logic</h2><p>In the pursuit of a flexible and testable code, we can adopt a different approach <em>without</em> relying on explicit dependency injection</p><p>We can achieve a clear separation of concerns by placing the I/O operations at the <strong>outermost layer</strong> of our code. Here&apos;s an example that demonstrates this concept:</p><pre><code class="language-python">def find_definition(word):           # Listing 3
    url = build_url(word)
    data = requests.get(url).json()  # I/O
    return pluck_definition(data)
</code></pre><p>Here, the <code>find_definition</code> function focuses solely on the core logic of extracting the definition from the received data. The I/O operations, such as making the HTTP request and retrieving the JSON response, are performed at the outer layer.</p><p>In addition, the <code>find_definition</code> function also relies on two separate functions:</p><ol><li><code>build_url</code> function constructs the URL for the API request</li><li><code>pluck_definition</code> function extracts the definition from the API response.</li></ol><p>Here are the corresponding code snippets:</p><pre><code class="language-python">def build_url(word):
    q = &apos;define &apos; + word
    url = &apos;http://api.duckduckgo.com/?&apos;
    url += urlencode({&apos;q&apos;: q, &apos;format&apos;: &apos;json&apos;})
    return url

def pluck_definition(data):
    definition = data[u&apos;Definition&apos;]
    if definition == u&apos;&apos;:
        raise ValueError(&apos;that is not a word&apos;)
    return definition
</code></pre><p>By putting I/O at the outermost layer, code becomes more flexible. <strong>We successfully created functions that can be individually tested and replaced as needed</strong>.</p><p>For example, you can easily switch to a different API endpoint by modifying the <code>build_url</code> function, or handle alternative error scenarios in the <code>pluck_definition</code> function. </p><p>This separation of concerns enables modifications to the I/O layer without impacting the core functionality of <code>find_definition</code>, enhancing the overall maintainability and adaptability of the codebase.</p><h3 id="updating-unit-tests-again">Updating unit tests (again)</h3><p>To demonstrate the improved flexibility and control offered by the modular design, let&apos;s update our unit tests for the <code>find_definition</code> function.</p><p>Here&apos;s the updated code snippet:</p><pre><code class="language-python">import unittest
from unittest.mock import patch

class TestFindDefinition(unittest.TestCase):
    @patch(&apos;requests.get&apos;)
    def test_find_definition(self, mock_get):
        mock_response = {&apos;Definition&apos;: &apos;Visit tournacat.com&apos;}
        mock_get.return_value.json.return_value = mock_response
        word = &apos;example&apos;
        expected_definition = &apos;Visit tournacat.com&apos;
        
        definition = find_definition(word)
        
        self.assertEqual(definition, expected_definition)
        mock_get.assert_called_once_with(build_url(word))
    
    def test_build_url(self):
        word = &apos;example&apos;
        expected_url = &apos;http://api.duckduckgo.com/?q=define+example&amp;format=json&apos;
        
        url = build_url(word)
        self.assertEqual(url, expected_url)
    
    def test_pluck_definition(self):
        mock_response = {&apos;Definition&apos;: &apos;What does tournacat.com do?&apos;}
        expected_definition = &apos;What does tournacat.com do?&apos;
        
        definition = pluck_definition(mock_response)
        self.assertEqual(definition, expected_definition)

if __name__ == &apos;__main__&apos;:
    unittest.main()
</code></pre><p>In the updated unit tests, we now have separate test methods for each of the modular components:</p><ol><li><code>test_find_definition</code> remains largely unchanged from the previous example before dependency injection was introduced, verifying the correct behavior of the <code>find_definition</code> function. However, it now asserts that the <code>requests.get</code> function is called with the URL generated by the <code>build_url</code> function, demonstrating the updated interaction between the modular components.</li><li><code>test_build_url</code> verifies that the <code>build_url</code> function correctly constructs the URL based on the given word.</li><li><code>test_pluck_definition</code> ensures that the <code>pluck_definition</code> function correctly extracts the definition from the provided data.</li></ol><p>By updating our unit tests, we can now test each component independently, ensuring that they function correctly in isolation.</p><h2 id="summary">Summary</h2><p>In short, we&#x2019;ve explored different approaches to refactoring to address tight coupling and achieve loose coupling between components. On top of that, we witnessed how unit testing can be enhanced by mocking I/O operations and controlling the behavior of external dependencies.</p><p>By placing I/O operations at the outermost layer of our code, we achieve a clear separation of concerns, enhancing the modularity and maintainability of our codebase.</p><p>Finally, if you&apos;re interested in reading more from me, <a href="https://jerrynsh.com/tag/python/">check out my other articles about Python</a>.</p>]]></content:encoded></item><item><title><![CDATA[I Built a Google Calendar Add-on. Here's What I Learnt]]></title><description><![CDATA[Read about the challenges encountered while building a Google Workspace add-on micro SaaS using Google Apps Script.]]></description><link>https://jerrynsh.com/i-built-a-google-calendar-add-on-heres-what-i-learnt/</link><guid isPermaLink="false">643f4f7f190620057213f805</guid><category><![CDATA[Tiny Project]]></category><category><![CDATA[Serverless]]></category><category><![CDATA[Google Apps Script]]></category><dc:creator><![CDATA[Jerry Ng]]></dc:creator><pubDate>Tue, 02 May 2023 00:00:21 GMT</pubDate><media:content url="https://jerrynsh.com/content/images/2023/04/home_1280_720.png" medium="image"/><content:encoded><![CDATA[<img src="https://jerrynsh.com/content/images/2023/04/home_1280_720.png" alt="I Built a Google Calendar Add-on. Here&apos;s What I Learnt"><p>I made <a href="https://tournacat.com/">Tournacat</a>, an add-on that syncs upcoming Esports schedules of tournaments to Google Calendar. Two months later, Tournacat has garnered 4 paid users and is slowly growing.</p><p>In this post, I will share my experience building Tournacat using various technologies including Cloudflare Worker, Hugo, and Google Apps Script.</p><p>Along the way, I encountered some interesting challenges and learned valuable lessons. Specifically, I want to share my anecdotal experience with issues I encountered while developing the Tournacat add-on using Google Apps Script.</p><h2 id="why-did-i-make-tournacat">Why Did I Make Tournacat</h2><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/04/month_1280_720.png" class="kg-image" alt="I Built a Google Calendar Add-on. Here&apos;s What I Learnt" loading="lazy" width="1280" height="720" srcset="https://jerrynsh.com/content/images/size/w600/2023/04/month_1280_720.png 600w, https://jerrynsh.com/content/images/size/w1000/2023/04/month_1280_720.png 1000w, https://jerrynsh.com/content/images/2023/04/month_1280_720.png 1280w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">A &quot;Month&quot; view of upcoming Esports schedules in Google Calendar.</span></figcaption></figure><p>Tournacat <a href="https://jerrynsh.com/sync-dota-2-esports-matches-to-your-google-calendar/">started as an idea from D2GCal</a>, a link to a Google Calendar featuring upcoming <a href="https://www.dota2.com/">Dota 2</a> tournaments available for purchase on <a href="https://jerrynsh.gumroad.com/l/d2gcal">Gumroad</a>.</p><p>I built Tournacat out of the frustration of missing out on big Dota 2 matches. I was tired of constantly scouring the internet to find out when my favorite matches were happening.</p><p>As an avid Esports fan, I knew there had to be a better way (<em>clich&#xE9; I know</em>). So, I started building Tournacat for my own use.</p><p>Today, Tournacat has grown to support major Esports titles including <em>Counter-Strike</em>, <em>Dota 2</em>,<em> LoL</em>,<em> Valorant</em>,<em> Overwatch</em>, <a href="https://tournacat.com/faqs/#what-games-and-esports-titles-are-covered">and more</a>.</p><h2 id="why-google-calendar">Why Google Calendar</h2><p>I use Google Calendar extensively in my daily life, from scheduling meetings to keeping track of personal events. So, having an Esports schedule alongside everything in one place is a huge plus.</p><p>With Tournacat, you can just <a href="https://workspace.google.com/marketplace/app/tournacat/1041160187344">install the add-on to your Google Calendar</a> and let it work its magic. It&apos;s a seamless solution that integrates perfectly with Google Calendar.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/04/schedule_1280_720.png" class="kg-image" alt="I Built a Google Calendar Add-on. Here&apos;s What I Learnt" loading="lazy" width="1280" height="720" srcset="https://jerrynsh.com/content/images/size/w600/2023/04/schedule_1280_720.png 600w, https://jerrynsh.com/content/images/size/w1000/2023/04/schedule_1280_720.png 1000w, https://jerrynsh.com/content/images/2023/04/schedule_1280_720.png 1280w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">A &quot;Schedule&quot; view of upcoming Esports schedules in Google Calendar.</span></figcaption></figure><h3 id="existing-solutions-calendars">Existing Solutions (Calendars)</h3><p>I&apos;ve been on the lookout for a solution like Tournacat for years. I&apos;ve tried all sorts of public Esports calendars in the past, but they always fell short in one way or another.</p><p>Either the author of the calendar stopped updating it, or it was just an unreliable source that was prone to human error. It got to the point where I gave up hope of ever finding a calendar that actually worked.</p><h2 id="tech-stack">Tech Stack</h2><p>Tournacat is built using a variety of tools and technologies. The tech stack includes:</p><ul><li>Frontend &#x2014; <a href="https://gohugo.io/">Hugo</a></li><li>Backend &#x2014; <a href="https://workers.cloudflare.com/">Cloudflare Worker</a></li><li>Add-on  &#x2014; <a href="https://developers.google.com/apps-script">Google Apps Script</a></li></ul><h3 id="hugo">Hugo</h3><p>Hugo is a <a href="https://www.gatsbyjs.com/docs/glossary/static-site-generator/#what-is-a-static-site-generator">static site generator</a> that I used to create the <a href="https://tournacat.com/">Tournacat website</a>. I decided to use Hugo for a couple of reasons.</p><p>First, I&apos;m not particularly skilled in front-end development, so I wanted to use a static site generator that would allow me to use pre-built themes without having to write much front-end code myself. Hugo fit the bill perfectly in this regard.</p><p>Second, Hugo is known for its speed and ease of use. I wanted to be able to iterate quickly and not have to spend a lot of time fussing with my site&apos;s performance or configuration. Hugo&apos;s documentation and tooling made it easy to get up and running quickly. With that, I was able to focus on building out the content for Tournacat&#x2019;s website rather than worrying about technical details.</p><p>I then hosted the website on <a href="https://pages.cloudflare.com/">Cloudflare Pages</a>. Using Cloudflare Pages allowed me to keep costs down, as I didn&apos;t need to pay for expensive hosting services.</p><h3 id="cloudflare-worker">Cloudflare Worker</h3><p>For the backend, I decided to go with Cloudflare Worker. Cloudflare Worker is a <a href="https://www.cloudflare.com/learning/serverless/what-is-serverless/">serverless computing</a> platform that allows you to run JavaScript code <a href="https://www.cloudflare.com/learning/serverless/glossary/what-is-edge-computing/">on the edge</a>.</p><p>To make my life easier, I decided to build the backend server Tournacat on top of a framework &#x2014; Worktop, a lightweight Cloudflare Worker web framework that simplifies the development of APIs using TypeScript.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/lukeed/worktop"><div class="kg-bookmark-content"><div class="kg-bookmark-title">GitHub - lukeed/worktop: The next generation web framework for Cloudflare Workers</div><div class="kg-bookmark-description">The next generation web framework for Cloudflare Workers - GitHub - lukeed/worktop: The next generation web framework for Cloudflare Workers</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.com/fluidicon.png" alt="I Built a Google Calendar Add-on. Here&apos;s What I Learnt"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">lukeed</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/e44527f3b710cb8af5863f9ce7f184d99eadd12dbf236788cf1669c4649e2a2d/lukeed/worktop" alt="I Built a Google Calendar Add-on. Here&apos;s What I Learnt"></div></a></figure><p>Given the opportunity to start over, I would consider using <a href="https://hono.dev/">Hono</a> instead, as it appears to be more actively maintained <a href="https://github.com/honojs/hono">on GitHub</a>.</p><p>My decision to choose Cloudflare Worker was based on my comfort level with it, <a href="https://jerrynsh.com/tag/cloudflare-worker/">having done several previous projects</a> with it. The developer experience was nothing short of amazing, and deployment was easy. Furthermore, it was <a href="https://developers.cloudflare.com/workers/platform/limits/#worker-limits">affordable with a generous free tier</a>.</p><p>While the performance was impressive, it was not the main reason why I picked it.</p><h3 id="google-apps-script">Google Apps Script</h3><p><a href="https://www.google.com/script/start/">Google Apps Script</a> is the core technology behind the <a href="https://workspace.google.com/marketplace/app/tournacat/1041160187344">Tournacat add-on</a>. It is a scripting language based on JavaScript that allows you to extend and automate Google Workspace products like Google Calendar, Sheets, and Drive.</p><p>While building Google Workspace add-ons is possible with <a href="https://developers.google.com/workspace/add-ons/guides/alternate-runtimes">other coding languages (runtimes)</a>, I decided to go for Google Apps Script anyway due to convenience. Frankly, I never thought that I&#x2019;d be impressed at how simple it is to build a workspace add-on!</p><p>Do keep in mind that it does come with <a href="https://developers.google.com/apps-script/guides/services/quotas">some limitations and quotas</a>.</p><h2 id="navigating-the-bumps">Navigating the Bumps</h2><p>Creating a Google Workspace add-on is relatively straightforward. The <a href="https://developers.google.com/apps-script/add-ons/cats-quickstart">add-on tutorial</a> provided by Google Workspace is pretty easy to follow.</p><p>In short, It took me a day to figure out most things and about a week to get the add-on approved and published on <a href="https://workspace.google.com/marketplace">Google Workspace Marketplace</a>.</p><h3 id="advantages">Advantages</h3><ol><li>One of the pros of using Google Apps Script is that it literally cost nothing to run.</li><li>Since it uses JavaScript, it&apos;s easy for most developers to pick up and start working with.</li></ol><h3 id="challenges">Challenges</h3><ol><li>Although Google claims that <a href="https://developers.google.com/apps-script/guides/v8-runtime">Apps Script is now supported by the V8 runtime</a>, it doesn&apos;t <em>actually</em> support Promises or async-await (<a href="https://issuetracker.google.com/issues/149937257">Issue Tracker</a>). Consequently, you may need to resort to <a href="https://stackoverflow.com/a/59750451">using workarounds like triggers</a> (Stackoverflow references: <a href="https://stackoverflow.com/questions/31241396/is-google-apps-script-synchronous">1,</a> <a href="https://stackoverflow.com/questions/61578224/does-google-apps-script-v8-engine-support-promise">2</a>).</li><li>Pushing changes to the Workspace add-on is rather unorthodox. I noticed this when one of my users couldn&#x2019;t get their time-based triggers to work properly after a new deployment. Turns out the &#x201C;correct&#x201D; way to update a workspace add-on is to <strong>edit</strong> a versioned deployment instead of creating a new one (<a href="https://stackoverflow.com/questions/69294697/workspace-add-on-with-installable-calendar-event-trigger-stops-working-with-new/69300465#69300465">Stackoverflow reference</a>).</li></ol><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/04/image.png" class="kg-image" alt="I Built a Google Calendar Add-on. Here&apos;s What I Learnt" loading="lazy" width="2000" height="650" srcset="https://jerrynsh.com/content/images/size/w600/2023/04/image.png 600w, https://jerrynsh.com/content/images/size/w1000/2023/04/image.png 1000w, https://jerrynsh.com/content/images/size/w1600/2023/04/image.png 1600w, https://jerrynsh.com/content/images/2023/04/image.png 2000w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Edit a versioned deployment with &quot;New version&quot; to update a workspace add-on</span></figcaption></figure><p>3. It has to respect the <a href="https://developers.google.com/apps-script/guides/services/quotas">limits set by Google</a>. However, it wasn&apos;t documented if the limits were shared across all add-on users cumulatively (<em>hint: it is not</em>).</p><p>4. Lastly, I came across an authentication-related issue (Stackoverflow references: <a href="https://stackoverflow.com/questions/65124449/trigger-error-were-sorry-a-server-error-occurred-while-reading-from-storage">1,</a> <a href="https://stackoverflow.com/questions/60442915/container-bound-script-getting-permission-errors-trying-to-run-functions-with-go">2</a>) when attempting to use Google Apps Script as a webhook to allow my users to automatically activate their subscriptions after payment. Some answers suggested <a href="https://stackoverflow.com/a/65124855">temporarily disabling the V8 runtime</a> to fix this.</p><p>Despite these hiccups, Google Apps Script is a great platform to work with. It gets 90% of the job done very effectively and the last 10% is the challenges that I mentioned.</p><h3 id="some-lessons-learned">Some Lessons Learned</h3><ol><li><strong>Use what makes you the most productive.</strong> Don&#x2019;t worry about picking the perfect tech stack that handles scale.</li><li><strong>Users couldn&#x2019;t care less about your code.</strong> Nobody except you cares about the fancy frameworks, design patterns, or architecture used.</li><li><strong>Keep your deployment simple.</strong> This allows for fast iterations. While I could have gone all-in with AWS and set up a sophisticated Terraform setup, I realized that I would have taken more time to ship if I were to go this route.</li></ol><h2 id="closing-thoughts">Closing Thoughts</h2><p>I have always had the itch to build a micro SaaS. So, I decided to build one realizing that it wouldn&#x2019;t make me rich or change the world. I simply enjoy building stuff that people want to use and money is the ultimate validation for that.</p><p>However, building a SaaS was not exactly what I thought it would be &#x2014; keeping my head down and coding away. I needed to understand my users and their needs and learn how to market my product effectively.</p><p>I must admit, marketing is not something I truly enjoy. But, I realized that it&apos;s a necessary job that comes with building a product.</p><p>Overall, building Tournacat was a rewarding experience. Seeing Tournacat slowly grow and gain traction has been an exciting journey. I&apos;m eager to continue on this journey and I look forward to what the future holds.</p>]]></content:encoded></item><item><title><![CDATA[Python For Else Construct: A Deep Dive]]></title><description><![CDATA[Discover the little-known for else construct in Python. Learn how and when to use the else clause after a loop effectively.]]></description><link>https://jerrynsh.com/python-for-else-construct-a-deep-dive/</link><guid isPermaLink="false">63e89cc2ffe2105bee4d2e54</guid><category><![CDATA[Python]]></category><dc:creator><![CDATA[Jerry Ng]]></dc:creator><pubDate>Mon, 03 Apr 2023 00:00:37 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1527266237111-a4989d028b4b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fGxvb3B8ZW58MHx8fHwxNjc2MTg5MzQy&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1527266237111-a4989d028b4b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDF8fGxvb3B8ZW58MHx8fHwxNjc2MTg5MzQy&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Python For Else Construct: A Deep Dive"><p>Can you use <em>else</em> in a <em>for</em> loop in Python? The answer is a resounding yes! I know it sounds crazy. The use of <code>else</code> after <code>for</code> and <code>while</code> loops in Python is not a widely known idiom. The use of the for-else construct is often misunderstood and overlooked.</p><p>For those who are coming from other programming languages, the syntax may be jarring. The fault is not on them though, the <code>else</code> clause is mostly associated only with the <code>if</code> statement.</p><p>But hold on, don&#x2019;t jump to conclusions just yet! The else clause in loops may seem strange at first, but it can be a useful tool in your Python toolkit.</p><h3 id="tldr">TL;DR</h3><ul><li>Think of <code>else</code> in a for-else construct as &#x201C;no break&#x201D;.</li><li>Consider using <code>else</code> with <code>for</code> or <code>while</code> loops instead of a flag variable to handle function <code>break</code> out of the loop.</li><li>An <code>else</code> clause is only useful with a preceding <code>break</code> in a loop. In other words, it&#x2019;s pointless to use the for-else construct without a <code>break</code> statement.</li></ul><h2 id="what-is-for-else-in-python">What is <em>for else</em> in Python</h2><p>Even today, the use of the for-else construct in Python remains largely unpopular. It often confuses even seasoned Python programmers.</p><p>Here&#x2019;s a trick to remember &#x2014; the <code>else</code> in a for-else construct can be remembered as <strong>&#x201C;no break&#x201D;</strong>.</p><p>It basically handles cases where <strong>no</strong> <code>break</code> statement is being executed within a loop. Easy, right?</p><p>But, when would you use the for-else construct in Python?</p><h2 id="when-and-how-to-use-else-in-a-for-loop">When and how to use <em>else</em> in a <em>for</em> loop</h2><p>A common use case is to go through a loop until something is found. When we find what we want, we would then break out of the loop and handle that case accordingly.</p><p>In short, we need to determine which case has happened:</p><ol><li>We found the target (<code>break</code> out early)</li><li>We finished the loop without finding the target</li></ol><p>One common method is to create a <a href="https://stackoverflow.com/a/17402180">flag variable</a> that will let us do a check later to see how the loop was exited.</p><p>Now, let&#x2019;s take a look at an example. Say you want to find the index of the first occurrence of a target in a <a href="https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range">sequence</a>. You can do this using a flag variable:</p><figure class="kg-card kg-code-card"><pre><code class="language-python">def find(seq, target):
    &quot;&quot;&quot;Find the index of the first occurrence of `target` in `seq`.&quot;&quot;&quot;

    found = False  # a flag variable
    for i, value in enumerate(seq):
        if value == target:
            found = True
            break

    if not found:
        return -1

    return i
</code></pre><figcaption><p><span style="white-space: pre-wrap;">In practice, you would want to use Python&#x2019;s </span><a href="https://docs.python.org/3/library/stdtypes.html?highlight=find#str.find"><span style="white-space: pre-wrap;">built-in find method</span></a><span style="white-space: pre-wrap;"> instead</span></p></figcaption></figure><p>Although this example is a little bit simplistic, code like this sometimes intermesh with other more complex code and there&#x2019;s no shortcut out (i.e. can&#x2019;t <code>return</code> the result immediately).</p><p>However, using the for-else construct, we can instead have:</p><figure class="kg-card kg-code-card"><pre><code class="language-python">def find(seq, target):
    &quot;&quot;&quot;Find the index of the first occurrence of `target` in `seq`.&quot;&quot;&quot;

    for i, value in enumerate(seq):
        if value == target:
            break

    else:  # no break
        return -1
    return i
</code></pre><figcaption><p><span style="white-space: pre-wrap;">Again in practice, you would want to use Python&#x2019;s built-in find method instead</span></p></figcaption></figure><p>Perhaps looking more elegant, this can simplify your code and improve readability instead of using a flag variable with an additional <code>if</code> check.</p><p>In short, the <code>else</code> clause in a <code>for</code> loop can be used to execute a block of code when the loop has finished running without encountering a <code>break</code> statement.</p><h3 id="for-fun-benchmark">For fun: benchmark</h3><p>As I was down this rabbit hole, I got a bit curious about how these 4 different methods would perform. With some semantical differences, these functions can be used to <em>find the index of the first occurrence of a character within a string.</em></p><p>Here&#x2019;s what I ran:</p><figure class="kg-card kg-code-card"><pre><code class="language-python">import cProfile

# finds the index of a target in a sequence with a flag variable
cProfile.run(&quot;&quot;&quot;
def flag_var_find(seq, target):    
    found = False
    for i, value in enumerate(seq):
        if value == target:
            found = True
            break
    
    if not found:
        return -1
    return i

flag_var_find(&apos;a&apos; * 100_000_000, &apos;b&apos;)
&quot;&quot;&quot;)

# finds the index of a target in a sequence using a for-else construct
cProfile.run(&quot;&quot;&quot;
def else_find(seq, target):
    for i, value in enumerate(seq):
        if value == target:
            break

    else:
        return -1
    return i
else_find(&apos;a&apos; * 100_000_000, &apos;b&apos;)
&quot;&quot;&quot;)

# finds the index of a target in a sequence using a generator expression
cProfile.run(&quot;&quot;&quot;
next((i for i, char in enumerate(&apos;a&apos; * 100_000_000) if char == &apos;b&apos;), -1)
&quot;&quot;&quot;)

# find the index of the first occurrence of a substring within a string
cProfile.run(&quot;&quot;&quot;
(&apos;a&apos; * 100_000_000).find(&apos;b&apos;)
&quot;&quot;&quot;)
</code></pre><figcaption><p><span style="white-space: pre-wrap;">You can copy and paste this into your ipython notebook. Note: strings are a special type of Sequence.</span></p></figcaption></figure><p>Here are the results:</p><pre><code>      4 function calls in 6.462 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.019    0.019    6.462    6.462 &lt;string&gt;:1(&lt;module&gt;)
      1    6.443    6.443    6.443    6.443 &lt;string&gt;:2(flag_var_find)
      1    0.000    0.000    6.462    6.462 {built-in method builtins.exec}
      1    0.000    0.000    0.000    0.000 {method &apos;disable&apos; of &apos;_lsprof.Profiler&apos; objects}

      4 function calls in 5.973 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.008    0.008    5.973    5.973 &lt;string&gt;:1(&lt;module&gt;)
      1    5.965    5.965    5.965    5.965 &lt;string&gt;:2(else_find)
      1    0.000    0.000    5.973    5.973 {built-in method builtins.exec}
      1    0.000    0.000    0.000    0.000 {method &apos;disable&apos; of &apos;_lsprof.Profiler&apos; objects}

      5 function calls in 5.763 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.007    0.007    5.763    5.763 &lt;string&gt;:1(&lt;module&gt;)
      1    5.756    5.756    5.756    5.756 &lt;string&gt;:2(&lt;genexpr&gt;)
      1    0.000    0.000    5.763    5.763 {built-in method builtins.exec}
      1    0.000    0.000    5.756    5.756 {built-in method builtins.next}
      1    0.000    0.000    0.000    0.000 {method &apos;disable&apos; of &apos;_lsprof.Profiler&apos; objects}

      4 function calls in 0.012 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.007    0.007    0.012    0.012 &lt;string&gt;:1(&lt;module&gt;)
      1    0.000    0.000    0.012    0.012 {built-in method builtins.exec}
      1    0.000    0.000    0.000    0.000 {method &apos;disable&apos; of &apos;_lsprof.Profiler&apos; objects}
      1    0.004    0.004    0.004    0.004 {method &apos;find&apos; of &apos;str&apos; objects}
</code></pre><p>Subtly, we can see that using the for-else construct is just a tiny bit faster than the flag variable way.</p><p>On the other hand, using Python&#x2019;s built-in string <code>find</code> method is significantly faster for this specific question. However, it will not work in the cases of other <a href="https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range">Sequence Types</a> like Lists or Tuples.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Jump into the rabbit hole of <a href="https://jerrynsh.com/tuples-vs-lists-vs-sets-in-python/">List vs. Tuple vs. Sets</a>.</div></div><h3 id="using-else-with-while-loops">Using <em>else</em> with <em>while</em> loops</h3><p>Here&#x2019;s a version of the same <code>find</code> function using a <code>while</code> loop instead of a <code>for</code> loop:</p><pre><code class="language-python">def find(seq, target):
    &quot;&quot;&quot;Find the index of the first occurrence of `target` in `seq`.&quot;&quot;&quot;

    i = 0
    while i &lt; len(seq):
        if seq[i] == target:
            return i
        i += 1
    else:
        return -1
</code></pre><h2 id="when-not-to-use-the-else-clause-in-a-loop">When not to use the else clause in a loop</h2><p>Simple. It&#x2019;s pointless to use a for-else construct without a preceding <code>break</code> statement in a loop.</p><h2 id="how-to-use-else-with-a-try-except-block">How to use <em>else</em> with a try-except block</h2><p>Loops aside, the <code>else</code> clause can also be used with a try-except block to run code when no exceptions were raised in the try block.</p><p>Similar to the for-else construct, the try-else construct is useful to distinguish between:</p><ol><li>The code ran successfully without any exception</li><li>The code ran into an exception</li></ol><pre><code class="language-python">try:
	book = find_book(id=1)

except BookNotFoundError:
    # handle the exception
	print(&quot;Book not found! :(&quot;)

else:
    # run this block if no exception was raised
	send_slack_notification(book)

finally: 
	# this block is always executed
	close_db_connection()
</code></pre><p>In this case, the code in the <code>else</code> block will only run if the code in the try block was executed successfully, without any exceptions.</p><p>In summary, consider using the try-else construct to separate the normal execution path from the error handling code.</p><h3 id="do-not-confuse-else-with-finally-in-try-blocks">Do not confuse <code>else</code> with <code>finally</code> in <code>try</code> blocks</h3><ul><li>try-else &#x2014; executes if there&#x2019;s NO exception</li><li>try-finally &#x2014; <strong>always</strong> gets executed regardless of exception</li></ul><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Tip: use <a href="https://jerrynsh.com/when-to-use-context-managers-in-python/">Python Context Manager</a> (with statement) to encapsulate the use of try-finally instead when it comes to exception handling.</div></div><h2 id="what%E2%80%99s-next">What&#x2019;s Next</h2><p>To summarize, the else clause in Python provides a way to execute code after a loop has completed execution without encountering a <code>break</code> statement.</p><p>I owe my thanks to this great talk by Raymond Hettinger, titled &#x201C;<a href="https://www.youtube.com/watch?v=OSGv2VnC0go">Transforming Code into Beautiful, Idiomatic Python</a>&#x201D;. In the talk, he briefly spoke about the history of the for-else construct. I highly recommend watching it &#x2014; I&#x2019;ve been using Python for 5+ years and I still learn many great tidbits in such a short time from him.</p><p>I personally would use the for-else construct whenever it makes sense. Having that said, I would recommend trying to get your team to be on the same page before adopting this widely unpopular construct.</p><p>Lastly, if you&#x2019;re interested to dive deeper into this rabbit hole, check out <a href="http://mail.python.org/pipermail/python-ideas/2009-October/006155.html">[Python-ideas] Summary of for...else threads</a>.</p>]]></content:encoded></item><item><title><![CDATA[How to Google With a Bang!]]></title><description><![CDATA[Get DuckDuckGo's Bang syntax on Google. Unlock the power of Chrome's Site Search to get DuckDuckGo Bang user experience with this simple trick. ]]></description><link>https://jerrynsh.com/how-to-google-with-a-bang/</link><guid isPermaLink="false">63c8da4f21669b2e2f70f5de</guid><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[Jerry Ng]]></dc:creator><pubDate>Wed, 01 Mar 2023 00:00:12 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1586125674857-4eb86880905d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDR8fGdvb2dsZSUyMHNlYXJjaHxlbnwwfHx8fDE2NzQxMDc0ODM&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1586125674857-4eb86880905d?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDR8fGdvb2dsZSUyMHNlYXJjaHxlbnwwfHx8fDE2NzQxMDc0ODM&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="How to Google With a Bang!"><p>I hopped on the <a href="https://duckduckgo.com/">DuckDuckGo</a> (DDG) hype train some years ago. It lasted for about 5 months. Unfortunately, I can&#x2019;t help to be constantly reminded how good Google site search is. A story probably for another day.</p><p>While DDG did not become my default search engine, there&#x2019;s one feature that changes how I look at browsers and search engines&#x2019; user experience (UX) forever &#x2014; <a href="https://duckduckgo.com/bangs">DuckDuckGo Bang</a>.</p><p>Trust me, people can go on quite a bit about the Bang (<code>!</code>) syntax in DDG. The address bar has become my main entry point to the Internet these days.</p><h3 id="tldr">TL;DR</h3><p>Replicate DDG&apos;s bang syntax in the Chrome address bar at <code>chrome://settings/searchEngines</code> and add new search engines under &#x201C;Site Search&#x201D;.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Firefox users, <a href="https://github.com/jameshealyio/bang-bookmarks/">check this out</a>.</div></div><h2 id="bangs">Bangs</h2><p>In DDG, Bang is nice as a quick shortcut that takes you to search results of other sites. </p><p>For example, if I enter <code>!r m1 macbook review</code> on <a href="https://duckduckgo.com/">DDG&#x2019;s search</a>, it takes me directly to Reddit search at <code>https://www.reddit.com/search?q=m1%20macbook%20review</code>.</p><p>It&apos;s a great way to cut through the noise and find the information that we need.</p><h2 id="google-was-my-first-stop">Google <em>was</em> my first stop</h2><p>Whenever I had to research anything, I&#x2019;d just simply Google it without any <a href="https://support.google.com/websearch/answer/2466433">fancy <a href="https://support.google.com/websearch/answer/2466433">search operators</a></a>. Anything from gadget reviews to technical searches like &#x201C;how to remove duplicates in a list in python&#x201D;.</p><p>But as of late, I find myself increasingly appending <code>site:reddit.com</code>, <code>site:github.com/user/repo</code>, <code>site:stackoverflow.com</code>, etc. to my search query to get more refined and meaningful results.</p><p>Doing so provides a relatively unbiased set of results that isn&#x2019;t tied to my browsing habit. Getting to reviews that are hopefully written by <em>actual</em> users.</p><h2 id="how-to-google-with-a-bang">How to Google With a Bang</h2><p>If you use Chrome, you can craft a similar UX as DDG Bang syntax. There&#x2019;s no need to install any extensions. It&#x2019;s available natively on your Chrome browser.</p><p>Here&apos;s an example:</p><figure class="kg-card kg-image-card kg-width-wide kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/02/google-with-a-bang.gif" class="kg-image" alt="How to Google With a Bang!" loading="lazy" width="976" height="639" srcset="https://jerrynsh.com/content/images/size/w600/2023/02/google-with-a-bang.gif 600w, https://jerrynsh.com/content/images/2023/02/google-with-a-bang.gif 976w"><figcaption>Google search showing how to Google with a DDG-like bang!</figcaption></figure><p>Here&#x2019;s how:</p><ol><li>Go to <code>chrome://settings/searchEngines</code> and add new search engines under <em>&#x201C;Site Search&#x201D;</em>.</li><li>Edit the search engine accordingly.</li></ol><ul><li>The &#x201C;Search engine&#x201D; field is a way to name your <em>&#x201C;Shortcut&#x201D;</em>, it can be anything.</li><li>Note that the example above uses the <strong>Google</strong> <strong>search engine.</strong></li><li>Alternatively, if you want to use the <strong>site&#x2019;s search engine</strong>, change the <em>&#x201C;URL with %s in place of query&#x201D;</em> to the site&#x2019;s search query. E.g. <code>https://www.reddit.com/search?q=%s</code> to search using Reddit&#x2019;s search instead.</li><li>You can even prepend <code>site:reddit.com/r/&lt;subreddit&gt;</code> for a more refined search.</li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/01/image.png" class="kg-image" alt="How to Google With a Bang!" loading="lazy" width="986" height="674" srcset="https://jerrynsh.com/content/images/size/w600/2023/01/image.png 600w, https://jerrynsh.com/content/images/2023/01/image.png 986w" sizes="(min-width: 720px) 720px"><figcaption>Example for appending &#x201C;site:reddit.com&#x201D; when prefixing &#x201C;!r&#x201D; shortcut to your search term</figcaption></figure><p>Example for appending &#x201C;site:reddit.com&#x201D; when prefixing &#x201C;!r&#x201D; shortcut to your search term</p><p>3. Repeat.</p><h3 id="some-ideas">Some ideas</h3><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://jerrynsh.com/content/images/2023/01/image-2.png" class="kg-image" alt="How to Google With a Bang!" loading="lazy" width="1308" height="670" srcset="https://jerrynsh.com/content/images/size/w600/2023/01/image-2.png 600w, https://jerrynsh.com/content/images/size/w1000/2023/01/image-2.png 1000w, https://jerrynsh.com/content/images/2023/01/image-2.png 1308w" sizes="(min-width: 720px) 720px"><figcaption>Some examples that I use</figcaption></figure><ul><li><code>!r</code> for <a href="http://reddit.com">reddit.com</a> &#x2014; product reviews and community-specific information.</li><li><code>!so</code> for <a href="http://stackoverflow.com">stackoverflow.com</a> &#x2014; software engineering, programming-related topics.</li><li><code>!p</code> for <a href="https://www.phind.com/">phind.com</a> &#x2013; Search engine optimized for developers. Add <code>https://phind.com/search?q=%s</code> and you&apos;re set!</li><li><code>!gh</code> for <a href="https://github.com/">github.com</a> &#x2014; libraries or framework-specific GitHub issues (e.g. bugs) and workarounds. This has been incredibly useful to me.</li><li><code>!hn</code> for <a href="http://hackernews.com">hackernews.com</a> &#x2014; &#xA0;I use this to look for discussions or anecdotes on specific topics.</li><li><code>!yt</code> for <a href="http://youtube.com">youtube.com</a> &#x2014; quicker (1-step) video search on YouTube.</li></ul><p>The possibilities are endless. Refer to <a href="https://duckduckgo.com/bangs">duckduckgo.com/bangs</a> for more ideas.</p><div class="kg-card kg-callout-card kg-callout-card-grey"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">In the case of Firefox, they make it quite easy to do the equivalent (<em>almost</em>). See how to <a href="https://support.mozilla.org/en-US/kb/assign-shortcuts-search-engines">assign shortcuts to search engines</a>. <br><br><em>Note: thanks to Brendan Keefe for pointing this out!</em></div></div><h3 id="useful-resources">Useful resources</h3><ul><li><a href="https://support.google.com/websearch/answer/134479?hl=en">How to search on Google</a></li><li><a href="https://support.google.com/websearch/answer/2466433">Refined web searches</a></li></ul><h2 id="closing-thoughts">Closing Thoughts</h2><p>Over the years, the quality of the top search results from Google has deteriorated:</p><ul><li>The first page is often filled with basic information that barely scratches the surface.</li><li>When it comes to software engineering or programming-related topics, it&#x2019;s the same problem. Outside of Stack Overflow, most articles are written to appeal to a broad audience.</li><li>SEO (page ranking) has become increasingly gamified. It probably killed all kinds of <em>real</em> reviews. That&#x2019;s why people have been gradually appending <code>site:reddit.com</code> to their Google searches because Reddit (and HN) is one of the few places online you can read actual human thoughts.</li></ul><p>I know <a href="https://dkb.io/post/google-search-is-dying">I am not the only one that feels this way</a>. A &#x201C;normal&#x201D; Google search now does not yield satisfactory results anymore.</p><p>Having said that, Google search is still very much superior in my opinion &#x2014; mostly because of Google Maps. </p><p>It&apos;s funny how we often take for granted the power of search engines, and how we drown in the sea of information that&apos;s available to us today.</p>]]></content:encoded></item><item><title><![CDATA[Python GIL: Explained Like I'm Five]]></title><description><![CDATA[ELI5 about what Python GIL is. Learn how GIL affects CPython and bytecode execution, and get a TL;DR of the ongoing debate surrounding the GIL.]]></description><link>https://jerrynsh.com/python-gil-explained-like-im-five/</link><guid isPermaLink="false">639ed70e93c361f74c79466e</guid><category><![CDATA[Python]]></category><dc:creator><![CDATA[Jerry Ng]]></dc:creator><pubDate>Wed, 01 Feb 2023 00:00:51 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1661201262769-fe601a5a84e3?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDE2fHxzb2RhJTIwbWFjaGluZXxlbnwwfHx8fDE2NzE0MjQ3MjU&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1661201262769-fe601a5a84e3?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDE2fHxzb2RhJTIwbWFjaGluZXxlbnwwfHx8fDE2NzE0MjQ3MjU&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Python GIL: Explained Like I&apos;m Five"><p>So, you have heard of the <a href="https://wiki.python.org/moin/GlobalInterpreterLock">Global Interpreter Lock</a>, or GIL for short? What exactly is GIL? Well, in short GIL is a mechanism that is built into the CPython interpreter. It is designed to prevent multiple native threads from executing Python bytecodes at once.</p><p>&#x201C;Blah-blah-blah, stop trying to sound smart! What the hell are CPython and bytecode?!&#x201D;. Hopefully, I haven&#x2019;t lost you yet at this point.</p><p>Before we start talking about GIL, let me briefly introduce to you what CPython and bytecode are to understand the <em>typical</em> answers about GIL that you read on the Internet.</p><p>Don&#x2019;t worry, it&#x2019;s not as scary as it sounds! Here&apos;s my GIL <a href="https://www.dictionary.com/e/slang/eli5/#:~:text=What%20does%20ELI5%20mean%3F,a%20complicated%20question%20or%20problem.">ELI5</a> take.</p><h2 id="what-is-cpython">What is CPython</h2><p>Just like how there are many different brands of soda (Coke, Pepsi, Mountain Dew, etc.), there are also many different versions of Python that you can use to write and run your programs.</p><p><a href="https://github.com/python/cpython">CPython</a> is like the &quot;original&quot; or &quot;standard&quot; version of the Python programming language. You&#x2019;re probably using it every day!</p><h3 id="reference-implementation">Reference implementation</h3><p>Written in C, CPython is called the &quot;<a href="https://wiki.python.org/moin/PythonImplementations">reference implementation</a>&quot; of Python, which means that it&apos;s the version that other versions of Python are compared to and tested against.</p><p>It&apos;s a bit like how Coke is the original soda that other sodas are based on or try to imitate.</p><h3 id="not-actually-compiling-to-c">Not actually compiling to C</h3><p>To be clear, CPython does <strong>not</strong> translate your Python code to C. The <a href="https://github.com/python/cpython"><a href="https://cython.org/">Cython project</a></a> on the other hand lets you compile your code to C.</p><h3 id="alternatives">Alternatives</h3><p>Several alternative implementations of Python are built to address different needs or to improve the performance of CPython in certain areas.</p><p>A non-exhaustive list of alternative implementations of Python includes:</p><ol><li><a href="https://www.pypy.org/">PyPy</a>: a fast, compliant implementation of Python that is built using a just-in-time compiler. It is often faster than CPython, particularly for larger programs.</li><li><a href="https://www.jython.org/">Jython</a>: an implementation of Python that is written in Java and can be used to run Python code on the Java Virtual Machine (JVM). This allows Python programs to integrate with Java programs and libraries.</li><li><a href="https://ironpython.net/">IronPython</a>: an implementation of Python built using the .NET framework and designed to be used with other .NET languages such as C#.</li></ol><h2 id="what-is-bytecode">What is bytecode</h2><p>Are you ready to learn about the mysterious world of Python bytecodes? Buckle up!</p><p>When you write a Python program and run it, the Python interpreter first compiles the source code into bytecodes and then executes the bytecodes to produce the desired output.</p><h3 id="soda-analogy">Soda analogy</h3><p>Imagine that you have a recipe for making the perfect soda. The recipe is written in a language that you can understand, like English.</p><p>But when you go to make the soda, you don&apos;t actually follow the recipe in English. You translate it into a series of instructions that your soda machine can understand &#x2013; these instructions are the &quot;bytecodes&quot;.</p><h3 id="came-across-pyc">Came across <code>.pyc</code>?</h3><p>In Python, bytecodes are a lower-level representation of the source code of a Python program.</p><p>They are typically stored in files with an <code>.pyc</code> extension, which stands for &quot;Python compiled code&quot;. These files are created automatically by the Python interpreter when it encounters a <code>.py</code> file that it needs to execute.</p><p>They are used to speed up the execution of Python programs by avoiding the need to recompile the source code each time the program is run.</p><p>So that&apos;s bytecode in a nutshell! It&apos;s a set of instructions that tells your computer what to do, kind of like a recipe for soda.</p><p>Just don&apos;t try to drink your bytecode &#x2014; that would be a bit weird.</p><h2 id="why-is-gil-necessary">Why is GIL necessary</h2><p>Well, you see, CPython wasn&#x2019;t designed to be thread-safe. In other words, multiple threads can potentially interfere with one another. This is bad because it can lead to something called &#x201C;<a href="https://en.wikipedia.org/wiki/Race_condition">race condition</a>&#x201D;.</p><p>The GIL makes our life easier as Python developers to write multi-threaded programs without worrying about race conditions.</p><h3 id="how-do-i-use-gil">How do I use GIL?</h3><p>In case you&#x2019;re wondering &#x2014; you <strong>don&apos;t</strong> need to do anything special to use the GIL in your Python code. It is implemented automatically in the CPython interpreter and is active by default.</p><p>However, you can use the <code>threading</code> module (<a href="https://docs.python.org/3/library/threading.html">docs</a>) to create and manage threads in your Python code:</p><pre><code class="language-python">import threading


def make_soda():
    print(&quot;Making a delicious soda!&quot;)

if __name__ == &quot;__main__&quot;:
    t1 = threading.Thread(target=make_soda)
    t2 = threading.Thread(target=make_soda)
    t3 = threading.Thread(target=make_soda)

    t1.start()
    t2.start()
    t3.start()
</code></pre><h3 id="race-condition">Race condition</h3><p>Imagine that you and your friends are making a batch of soda together. You&apos;ve got all the ingredients ready to go &#x2014; sugar, water, caramel coloring, and some secret spices.</p><p>But you all try to add the ingredients to the same pot at the same time. This can lead to unexpected results because you don&apos;t know which ingredients will be mixed in first. This is similar to a race condition in a program, where two or more threads try to modify the same shared resource at the same time!</p><p>But because the GIL is in place, the other thread will have to wait for its turn before it can start making some fizzy goodness.</p><p>You may have already noticed &#x2014; GIL may seem like a bottleneck because of this wait. But, it&#x2019;s actually just there to keep things running smoothly.</p><h3 id="inputoutput-io-bound">Input/Output (I/O) Bound</h3><p>It is important to note that GIL does <strong>not</strong> affect programs that rely on I/O operations as these operations typically release the GIL while waiting for data to be transferred.</p><p>What exactly do you mean by I/O bound?</p><p>At its most basic, I/O bound means that a program is waiting for I/O operations to complete. This can include things like:</p><ol><li>Reading from or writing to a file</li><li>Sending or receiving data over a network</li><li>Interacting with a user through a graphical user interface (GUI), etc.</li></ol><p>Essentially, any time a program has to wait for something to happen outside of itself, it&apos;s probably doing I/O-bound work.</p><h2 id="the-debate">The Debate</h2><p>A TL;DR about all the fuss about the Global Interpreter Lock, or GIL.</p><h3 id="supporters">Supporters</h3><blockquote class="kg-blockquote-alt">&#x201C;The GIL is a great feature of Python! It makes it easy for us to write multi-threaded programs without having to worry about race conditions!&#x201D; &#x2013; says the supporters.</blockquote><p>They argue that <em>most</em> Python programs aren&#x2019;t CPU-intensive, to begin with. Most of the time, the programs are doing I/O-bound work which GIL doesn&#x2019;t really affect. So, why all the fuss?</p><h3 id="critics">Critics</h3><blockquote class="kg-blockquote-alt">&#x201C;The GIL is holding us back!&#x201D; &#x2013; say the critics with an opposite view.</blockquote><p>With GIL, only one thread can execute Python bytecodes at a time.</p><p>As a result, this can limit the performance of CPU-bound Python programs on systems with multiple CPU cores. Because of this, GIL is known as a major limitation of Python.</p><p>Although most Python programs are I/O-bound, it doesn&#x2019;t mean that we shouldn&#x2019;t be able to write high-performance CPU-bound programs in Python.</p><h2 id="conclusion">Conclusion</h2><p>So who&apos;s right? Well, as with most debates, there&apos;s no easy answer.</p><p>The GIL may be a necessary compromise that makes it easy for Python programmers to write multi-threaded programs. Nevertheless, it can also limit the performance of CPU-bound programs on systems with multiple CPU cores.</p><p>The debate lives on, for now, I guess. Lastly, check out <a href="https://news.ycombinator.com/item?id=28690560">this HN post and comments</a> if you&apos;re interested to dive a little bit deeper into the wonderful world of GIL.</p>]]></content:encoded></item><item><title><![CDATA[Python Decorator: Explained in What Why When]]></title><description><![CDATA[Discover the basics of decorator functions in Python and how to use them to add functionality to your code. Learn when to use Python decorators.]]></description><link>https://jerrynsh.com/python-decorators-explained-in-what-why-when/</link><guid isPermaLink="false">63917ebd93c361f74c794621</guid><category><![CDATA[Python]]></category><dc:creator><![CDATA[Jerry Ng]]></dc:creator><pubDate>Tue, 03 Jan 2023 00:00:39 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1607083681678-52733140f93a?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDMxfHxnaWZ0fGVufDB8fHx8MTY3MTQ1Mjc2Mw&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1607083681678-52733140f93a?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDMxfHxnaWZ0fGVufDB8fHx8MTY3MTQ1Mjc2Mw&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Python Decorator: Explained in What Why When"><p>Decorators are incredibly powerful in Python. They are useful for creating modular and reusable code. Plus, they&apos;re pretty cool once you get the hang of them!</p><p>In this blog post, we&apos;ll take a look at what decorators are, why you should use them, and when you should and shouldn&apos;t use them.</p><p>As always, I&#x2019;ll try to explain everything in layman&apos;s terms. My goal is to get you comfortable with decorators.</p><h3 id="tldr">TL;DR</h3><ul><li>Think of Python decorators as little &quot;wrappers&quot; that you can place around a function (or another object) to modify its behavior in some way.</li><li>You would typically use decorators for logging, authentication, or to make a function run faster by caching its result.</li></ul><h2 id="what-are-decorators">What are Decorators</h2><p>Let&#x2019;s start with a simple explanation of what decorators are in Python.</p><p>A decorator is really just <strong>a function that takes in a function as an argument</strong>. It then returns a modified version of that function.</p><p>Here&#x2019;s a classic example:</p><pre><code class="language-python">def my_decorator(func):
    def wrapper():
        print(&quot;Before the function is called.&quot;)
        func()
        print(&quot;After the function is called.&quot;)
    return wrapper
</code></pre><ul><li>In this example, the <code>my_decorator</code> function takes a function as an argument (which we&apos;ll call <code>func</code>) and defines a new inner function called <code>wrapper</code>. </li><li>The <code>wrapper</code> function prints a message before and after calling the original <code>func</code> function. </li><li>The <code>my_decorator</code> function then returns the <code>wrapper</code> function.</li></ul><p>To use a decorator, we would apply it to a function using the <code>@</code> symbol, e.g. <code>@my_decorator</code>. Here&#x2019;s an example:</p><pre><code class="language-python">def my_decorator(func):
    ...

@my_decorator
def greet_ben():
    print(&quot;Hello, Ben!&quot;)
</code></pre><p>If we were to call <code>greet_ben</code>, the output would look like this:</p><pre><code class="language-bash">Before the function is called.
Hello, Ben!
After the function is called.
</code></pre><p>See how we effectively modified <code>greet_ben</code> to print additional messages without changing the code of the original function itself?</p><h3 id="syntactic-sugar">Syntactic sugar</h3><p>You see, the decorator syntax <code>@my_decorator</code> is merely syntactic sugar. The 2 following function definitions are semantically equivalent:</p><pre><code class="language-python">def my_decorator(func):
    ...

# Function definition 1
def greet_ben():
    ...
greet_ben = my_decorator(greet_ben)

# Function definition 2
@my_decorator
def greet_ben():
    ...
</code></pre><p>Interesting, right?</p><h3 id="python-decorator-template">Python decorator template</h3><p>Now, hold your thought for a second, that was not how we would typically write a decorator function in Python &#x2014; there are better ways.</p><p>Instead, the following template can be used as a starting point for creating custom decorators:</p><figure class="kg-card kg-code-card"><pre><code class="language-python">from functools import wraps

def decorator_function(func):
    &quot;&quot;&quot;
    A generic decorator function template.
    
    :param func: The function to be decorated.
    :return: The decorated function.
    &quot;&quot;&quot;
    @wraps(func)
    def wrapper(*args, **kwargs):
        &quot;&quot;&quot;
        The wrapper function that will be executed when the decorated function is called.
        
        :param *args: Positional arguments passed to the decorated function.
        :param **kwargs: Keyword arguments passed to the decorated function.
        :return: The result of the decorated function.
        &quot;&quot;&quot;
        # Add any pre-function execution code here.
        # For example, logging, timing, or input validation.

        result = func(*args, **kwargs)  # Call the decorated function.

        # Add any post-function execution code here.
        # For example, logging, timing, or output validation.

        return result

    return wrapper</code></pre><figcaption><p><span style="white-space: pre-wrap;">Check out </span><a href="https://gist.github.com/jaantollander/48480cfabbe98a7efe4c9848a5b55e6a"><span style="white-space: pre-wrap;">this GitHub Gist</span></a><span style="white-space: pre-wrap;"> for more Python decorator templates</span></p></figcaption></figure><p>You can modify the pre-function and post-function execution code sections to implement the desired behavior for your specific use case.</p><h3 id="why-do-we-need-functoolswraps">Why do we need <code>functools.wraps</code>?</h3><p>Using <code>from functools import wraps</code> and applying the <code>@wraps(func)</code> decorator to the <code>wrapper</code> function is <strong>not strictly necessary</strong>, but it provides some important benefits when creating decorators.</p><p>When you create a decorator without using <code>wraps</code>, the decorated function&apos;s metadata, such as its name, docstring, and module, are replaced by the metadata of the <code>wrapper</code> function. This can lead to confusion and make debugging more difficult.</p><p>Here&apos;s an example to illustrate the issue:</p><pre><code class="language-python">def decorator_function(func):
    def wrapper():
        func()
    return wrapper

@decorator_function
def my_function():
    &quot;&quot;&quot;This is my_function&apos;s docstring.&quot;&quot;&quot;
    pass

print(my_function.__name__)  # Output: &apos;wrapper&apos;
print(my_function.__doc__)   # Output: None</code></pre><p>As you can see, the name and docstring of <code>my_function</code> are lost when it&apos;s decorated without using <code>wraps</code>.</p><p>Now, let&apos;s use <code>wraps</code>:</p><pre><code class="language-python">from functools import wraps

def decorator_function(func):
    @wraps(func)
    def wrapper():
        func()
    return wrapper

@decorator_function
def my_function():
    &quot;&quot;&quot;This is my_function&apos;s docstring.&quot;&quot;&quot;
    pass

print(my_function.__name__)  # Output: &apos;my_function&apos;
print(my_function.__doc__)   # Output: &quot;This is my_function&apos;s docstring.&quot;</code></pre><p>By using <code>@wraps(func)</code>, the decorated function retains its original metadata, making it easier to understand and debug. </p><p>Overall, using <code>functools.wraps</code> is highly recommended when creating decorators in Python as it helps to create more reliable, maintainable, and compatible decorators that work well with other tools and libraries.</p><h2 id="why-use-decorators">Why Use Decorators</h2><p>Understanding the &#x201C;why&#x201D; is kind of like adding the icing on the cake &#x2014; it just makes the whole thing a lot sweeter. So why use decorators at all?</p><p>Well, decorators provide a flexible and convenient way to add extra functionality to existing functions, making them more <em>reusable</em> and <em>modular</em>.</p><p>This is useful when you want to add a feature to a function that is used in multiple places in your code, but you don&apos;t want to modify the original function itself.</p><h3 id="avoid-cluttering">Avoid cluttering</h3><p>For example, you might want to add logging or error handling to a function. But, you don&apos;t want to clutter the original function with that extra code.</p><p>By using a decorator, you can add extra functionality without changing the original function, making your code cleaner and more organized.</p><h2 id="when-to-use-decorators">When to Use Decorators</h2><p>Now that we know why we should use decorators, let&apos;s talk about when to use them.</p><p>Generally speaking, Python decorators are most useful when you want to add extra functionality to a function without modifying its code. This might include things like:</p><ol><li>Logging</li><li>Error handling</li><li><a href="https://docs.python.org/3/library/functools.html#functools.cache">Caching</a></li><li>Authentication (<a href="https://circleci.com/blog/authentication-decorators-flask/">see example in Flask</a>)</li><li>Timing a function execution (<a href="https://gist.github.com/Integralist/77d73b2380e4645b564c28c53fae71fb">see an advanced example</a>)</li><li><a href="https://github.com/litl/backoff">Backoff and retry</a></li></ol><h3 id="a-caching-example">A caching example</h3><p>Here&apos;s an example of how you might implement a simple cache for a Fibonacci function:</p><pre><code class="language-python">cache = {}

def fib(n):
    if n in cache:
        return cache[n]

    if n &lt;= 1:
        return n

    result = fib(n - 1) + fib(n - 2)
    cache[n] = result
    return result

print(fib(100)) # 354224848179261915075
</code></pre><p>Notice that the caching code lives inside the <code>fib</code> function itself.</p><p>However, with decorators, we can simply define a caching decorator and use it to add caching functionality to our Fibonacci function:</p><pre><code class="language-python">cache = {}

def cache_results(func):
    def wrapper(n):
        if n in cache:
            return cache[n]

        result = func(n)
        cache[n] = result
        return result
    return wrapper

@cache_results
def fib(n):
    if n &lt;= 1:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(100)) # 354224848179261915075
</code></pre><ul><li>Here, we&apos;ve implemented our own simple cache using a dictionary.</li><li>We&apos;ve also defined our own decorator function <code>cache_results</code> that takes a function as an argument and returns a new function that wraps the original function.</li><li>This inner function checks the cache to see if a result has already been calculated, and if so, it returns the cached result.</li><li>Otherwise, it calculates the result and adds it to the cache before returning it.</li><li>This way, we can apply the <code>cache_results</code> decorator to any function we want to add caching for.</li></ul><p>While both examples work, the latter allows us to easily add caching to other functions without having to write the caching code inside each function.</p><p>This makes the caching code in our example more reusable and modular!</p><h3 id="a-better-example-with-built-in-modules">A better example with built-in modules!</h3><p>Instead of reinventing the wheel, we can use the <code>lru_cache</code> decorator from the <code>functools</code> module to add caching to your <code>fib</code> function:</p><pre><code class="language-python">from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n &lt;= 1:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(100)) # 354224848179261915075
</code></pre><ul><li>Using <code>lru_cache</code> decorator is really easy &#x2013; all you have to do is add the decorator to your function and it automatically handles the caching for you.</li><li>So, using <code>lru_cache</code> can be a great way to optimize your code without a lot of extra effort.</li></ul><p>Again, this is just one example of how you can use decorators to make your code more efficient and elegant.</p><h2 id="when-not-to-use-decorators">When NOT to Use Decorators</h2><p>So, when should you avoid using decorators?</p><p>Well, decorators are great for keeping your code clean and easy to read, but they can make your code harder to understand if overused or used incorrectly.</p><p>In general, it&apos;s best to avoid using decorators if they make your code more complex than it needs to be, or if you&apos;re not sure how they work.</p><p>For instance, if you&apos;re working on a very small project and you don&apos;t need to keep track of how many times your functions are called, then you probably don&apos;t need to use a decorator.</p><p>In some cases, you may find that it might be more appropriate to modify the code of the original function directly, rather than using a decorator.</p><p>It&apos;s always a good idea to keep things simple and easy to understand!</p><h2 id="summary">Summary</h2><p>In short, Python decorators are like little &quot;wrappers&quot; that you can place around a function (or another object) to modify its behavior in some way.</p><p>Decorators are useful for creating modular and reusable code in Python.</p><p>You can define a decorator once and then apply it to any number of functions. This allows you to easily add the same extra functionality to multiple functions without repeating yourself.</p>]]></content:encoded></item><item><title><![CDATA[Say Goodbye to Heroku Free Tier: Here Are 4 Alternatives]]></title><description><![CDATA[Looking for alternatives to Heroku free tier? Check out these 4 Heroku free tier alternatives that offer similar features at affordable prices.]]></description><link>https://jerrynsh.com/bid-farewell-to-heroku-free-tier/</link><guid isPermaLink="false">6379cc60fb49e6cc86b1af31</guid><category><![CDATA[Heroku]]></category><category><![CDATA[PaaS]]></category><dc:creator><![CDATA[Jerry Ng]]></dc:creator><pubDate>Thu, 01 Dec 2022 00:00:43 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1667372459607-2cfe842fdc4b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDl8fGt1YmVybmV0ZXN8ZW58MHx8fHwxNjY4OTI2NzA0&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1667372459607-2cfe842fdc4b?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDl8fGt1YmVybmV0ZXN8ZW58MHx8fHwxNjY4OTI2NzA0&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Say Goodbye to Heroku Free Tier: Here Are 4 Alternatives"><p>28 November 2022 was a sad day for developers. If you haven&apos;t heard, Salesforce (Heroku&#x2019;s parent organization) has phased out its free tier plan on this date.</p><p>For many years, Heroku has been the de facto standard platform as a service (PaaS). So many students and developers deployed their first web application on Heroku. Anecdotally, Heroku has been pivotal for my career.</p><p><em>TL;DR, if you&#x2019;re looking for Heroku free tier alternatives, check out <a href="https://free-for.dev/#/?id=paas">free-for.dev/#/?id=paas</a>. I migrated my Cron jobs to <a href="https://northflank.com/pricing">Northflank</a> and Heroku Dynos to <a href="https://www.koyeb.com/">Koyeb</a>.</em></p><h3 id="a-lot-has-happened">A lot has happened</h3><p>For context, a lot has happened in the year 2022 for Heroku. The two most notable events are:</p><ol><li>On April 2022, Heroku had a security breach (<a href="https://status.heroku.com/incidents/2413">incident</a>) where CI and Review App secrets were compromised. GitHub Actions integrations on Heroku were down for a couple of months. Comms from Heroku were objectively bad. I experienced this firsthand when I had to resort to using a <a href="https://github.com/marketplace/actions/deploy-to-heroku">GitHub CI Action</a> for my app deployments.</li><li>On August 2022, <a href="https://blog.heroku.com/next-chapter">Heroku announced the removal of their free product plans</a>.</li></ol><p>If you have been keeping an eye on the Internet, you would see people expressing concerns about how <a href="https://news.ycombinator.com/item?id=31390506">Heroku is losing its magic</a>. Ever since the security breach incident, I see more talks about Heroku alternatives taking place.</p><p>Though things started to look bad, whatever Heroku had to offer was still great and I decided to stick to it. Until now, that is.</p><h2 id="heroku-alternatives">Heroku Alternatives</h2><p>For the past couple of weeks, I have been moving my demos and <a href="https://jerrynsh.com/tag/tiny-project/">tiny projects</a> out of Heroku.</p><p>If you&#x2019;re still looking for Heroku free tier alternatives, check out <a href="https://free-for.dev/#/?id=paas">free-for.dev</a> (<a href="https://github.com/ripienaar/free-for-dev">GitHub</a>).</p><p>While there are a bunch of blog posts and recommendations scattered on the Internet now, free-for.dev provides the best coverage. The list comes with a brief description of what each Heroku alternative does.</p><h3 id="what-i-was-looking-for">What I was looking for</h3><p>Do note that my use case is mostly for small-scale personal projects. So, whatever I have written may not be ideal for your use case.</p><ol><li>Equivalent <strong>free tiers</strong> in terms of CPU and RAM. (<em>spoiler: none of them were as good</em>)</li><li>Support for Cron jobs out of the box. Ideally, no weird workaround is required</li><li>Custom domain</li><li>GitHub integration</li><li>Support for basic logging and monitoring (i.e. CPU, memory, disk, network metrics)</li></ol><p>The bandwidth cost and supported region of each PaaS were not my primary concerns.</p><h3 id="my-picks">My picks</h3><p><em>Heroku Postgres</em> &#x2014; since May 2022, <a href="https://jerrynsh.com/saying-goodbye-to-heroku-postgres/">I have migrated from Heroku Postgres to Railway</a>. So far, I have no complaints about it. Another alternative that I was looking at was <a href="https://planetscale.com/">PlanetScale</a>. However, I didn&#x2019;t go for it because it isn&#x2019;t Postgres-compatible.</p><p><em>Heroku Dynos apps</em> &#x2014; <a href="https://burplist.com/">Burplist</a> was migrated to <a href="https://www.koyeb.com/tutorials/migrate-from-heroku">Koyeb</a>. Having tried out other notable Heroku alternatives like <a href="https://fly.io/docs/rails/getting-started/migrate-from-heroku/">Fly.io</a>, <a href="https://northflank.com/docs/">Northflank</a>, and <a href="https://railway.app/heroku">Railway</a>, I can safely say that the migration from Heroku to Koyeb required the least amount of effort and kinks. It just works in my case.</p><p><em>Heroku Scheduler</em> &#x2014; for my Cron jobs, I opted for Northflank. Their support for Cron jobs is by far the best. Pricing aside, the developer experience is much better than Heroku Scheduler.</p><p><em>Heroku Add-ons</em> &#x2014; not applicable to my case. I was relying on Heroku Add-ons for monitoring and logging. Thankfully most of the modern PaaS today support that out of the box.</p><h3 id="some-thoughts">Some thoughts</h3><p>Woefully, most Heroku alternatives&#x2019; free plans aren&#x2019;t as good as Heroku. For example, <em>most</em> of the free compute instances provided are 1 shared CPU and 256 MB RAM (Heroku Free Dynos started at 512 MB RAM). Not considering the limited number of apps allowed yet.</p><p>To my surprise, most of these PaaS still don&#x2019;t support running Cron jobs natively. Shoutout to Northflank again for providing such capability with great developer experience.</p><p>For Koyeb, the supported regions for free tiers are quite limited at the time of writing this.</p><p>Lastly, <a href="https://render.com/pricing">render.com</a> is another popular alternative out there in the market. You may want to check them out.</p><h2 id="cost-comparisons">Cost Comparisons</h2><p><em>Updated as of 1 Dec 2022.</em></p><h3 id="free-tiers">Free tiers</h3><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th></th>
<th><a href="https://www.heroku.com/dynos">Heroku</a></th>
<th><a href="https://fly.io/docs/about/pricing/">Fly.io</a></th>
<th><a href="https://railway.app/pricing">Railway.app</a></th>
<th><a href="https://northflank.com/pricing">Northflank.com</a></th>
<th><a href="https://www.koyeb.com/pricing">Koyeb.com</a></th>
</tr>
</thead>
<tbody>
<tr>
<td>CPU</td>
<td>1</td>
<td>1</td>
<td>0.1</td>
<td>0.1</td>
<td>1</td>
</tr>
<tr>
<td>RAM</td>
<td>512 MB</td>
<td>256 MB</td>
<td>300 MB</td>
<td>256 MB</td>
<td>256 MB</td>
</tr>
<tr>
<td>GitHub Integration for CD</td>
<td>Yes</td>
<td><a href="https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/">Yes, but through GitHub Actions</a></td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Note</td>
<td>Free tier before 28 November</td>
<td>Up to 3 VMs</td>
<td>Based on $5/mo free credit</td>
<td></td>
<td>$2.7/mo based on $5/mo free credit</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><h3 id="pricing">Pricing</h3><p>Free tiers aside, most of these Heroku alternatives bill you for the resources you use <strong>by the second/minute/hour</strong>.</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th></th>
<th><a href="https://www.heroku.com/dynos">Heroku</a></th>
<th><a href="https://fly.io/docs/about/pricing/">Fly.io</a></th>
<th><a href="https://railway.app/pricing">Railway.app</a></th>
<th><a href="https://northflank.com/pricing">Northflank.com</a></th>
<th><a href="https://www.koyeb.com/pricing">Koyeb.com</a></th>
</tr>
</thead>
<tbody>
<tr>
<td>CPU</td>
<td>$5&#xA0;for 1000 dyno hours/mo&#x2014; $500/dyno/mo</td>
<td>$1.94/mo &#x2014; $558.16/mo; 1 shared &#x2014; 8 dedicated</td>
<td>$20 / vCPU / mo</td>
<td>$2.71/mo &#x2014; $480.00/mo; 0.1 shared &#x2014; 20 dedicated</td>
<td>$2.7/mo &#x2014; $85.7/mo; 1 &#x2014; 8</td>
</tr>
<tr>
<td>RAM</td>
<td>Same as CPU; 512 MB &#x2014; 14 GB</td>
<td>Same as CPU; 256 MB &#x2014; 64 GB</td>
<td>$10 / GB / mo</td>
<td>Same as CPU; 256 MB &#x2014; 40.96 GB</td>
<td>Same as CPU; 256 MB &#x2014; 8 GB</td>
</tr>
<tr>
<td>Note</td>
<td></td>
<td></td>
<td></td>
<td>Price differs for jobs</td>
<td></td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><p>One thing that I did not cover here is <strong>bandwidth cost</strong> (inbound + outbound). This is something you might want to seriously consider for bigger projects.</p><h2 id="they-feel%E2%80%A6-different">They feel&#x2026; different</h2><p>Perhaps I am biased. There aren&#x2019;t any obvious drop-in replacements for Heroku free tiers (at least not on par with Heroku&#x2019;s generosity). Throwing free plans aside, most of them don&#x2019;t offer the same kind of developer experience as Heroku.</p><p>Heroku free tiers are one of the best things that have ever happened to software engineering in my opinion.</p><p>I really appreciated <a href="https://devcenter.heroku.com/articles/free-dyno-hours">Heroku&#x2019;s free dyno hours</a>. I didn&#x2019;t mind the fact that dynos goes to sleep and only gets woken up when it&#x2019;s needed &#x2014; this means I could host multiple demos at once and only consume my free limits on demand.</p><p>Need to show something to someone quickly? Simply send them your URL. It&#x2019;ll be up in a couple of seconds and I am totally cool with the cold start. This developer experience is something all the existing platforms cannot give, at least not with their free plans.</p><p>While I&#x2019;m a happy paying customer, it stings to pay $5/month for something that I do not use 99% of the time.</p><h2 id="final-thoughts">Final Thoughts</h2><p>Sadly, it is how it is. The Internet is a gnarly place. If you are offering free service on the Internet, people will find a way to abuse it. I remember <a href="https://jerrynsh.com/i-built-my-own-tiny-url/">hosting my first URL shortener</a>. On my first day of making it public, the service immediately got spammed. I had to implement CAPTCHA and expiring links to mitigate abuse.</p><p>Today, whenever I see something available for &#x201C;truly free&#x201D; (<em>if you know what I mean</em>) created by individuals, I can&#x2019;t help but wonder how are they going to keep the lights on sustainably out of goodwill. I just hate to see someone&#x2019;s good intentions fail. <a href="https://news.ycombinator.com/item?id=33432820">Maybe we should start paying for Internet stuff</a>.</p><p>Though I use cloud services like AWS almost every day, PaaS like Heroku always holds a special place in my heart.</p><p>Heroku was amazing. I think it did a great job of raising the bar in the PaaS field besides indirectly advocating free education (in one way or another).</p><p>Hats off to you Heroku.</p>]]></content:encoded></item></channel></rss>