<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lucy Linder</title>
    <description>The latest articles on DEV Community by Lucy Linder (@derlin).</description>
    <link>https://dev.to/derlin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F678845%2F70a4210c-2961-42c0-bca6-682f88e4b579.png</url>
      <title>DEV Community: Lucy Linder</title>
      <link>https://dev.to/derlin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/derlin"/>
    <language>en</language>
    <item>
      <title>Understanding Cloudflare Caching: What Gets Cached and How to Control It</title>
      <dc:creator>Lucy Linder</dc:creator>
      <pubDate>Mon, 17 Mar 2025 13:10:00 +0000</pubDate>
      <link>https://dev.to/derlin/understanding-cloudflare-caching-what-gets-cached-and-how-to-control-it-1fee</link>
      <guid>https://dev.to/derlin/understanding-cloudflare-caching-what-gets-cached-and-how-to-control-it-1fee</guid>
      <description>&lt;p&gt;I used to think that Cloudflare caching just worked out of the box - set your website behind Cloudflare, and boom! Your HTML and media pages are cached, performance increases, and your boss is happy.&lt;/p&gt;

&lt;p&gt;Well… not quite. Cloudflare is a great tool, but to truly take advantage of it, you must take the time to understand how it works. Especially, you may have to set up a somewhat counter-intuitive rule. Curious? Let’s break it down!&lt;/p&gt;

&lt;p&gt;ℹ &lt;strong&gt;BEFORE WE START&lt;/strong&gt; ℹ This article is far from complete, I couldn’t cover all of Cloudflare’s cache nuances without losing my audience (and myself). Instead, I will focus on key concepts and configurations, assuming you already know how to set up an application behind Cloudflare.&lt;/p&gt;




&lt;p&gt;In this article:&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;Cloudflare Cache Statuses&lt;/li&gt;
&lt;li&gt;What is a “resource”&lt;/li&gt;
&lt;li&gt;Which resources are eligible for cache&lt;/li&gt;
&lt;li&gt;Which eligible resources are cached&lt;/li&gt;
&lt;li&gt;How to extend cached resources to HTML (and others)&lt;/li&gt;
&lt;/ul&gt;






&lt;h2&gt;
  
  
  Cloudflare Cache Statuses
&lt;/h2&gt;

&lt;p&gt;First, let’s understand the headers and tools Cloudflare gives us to understand how our pages are cached (or not). When a page is served through Cloudflare, it adds some &lt;code&gt;Cf-*&lt;/code&gt; cache headers.&lt;/p&gt;

&lt;p&gt;The most important one is &lt;code&gt;Cf-Cache-Status&lt;/code&gt;, which can take the following values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;HIT&lt;/code&gt; → cacheable resource, served from the cache.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;MISS&lt;/code&gt; → cacheable resource, never cached (or at least not in the cache at the time of the request). Served from the origin.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;EXPIRED&lt;/code&gt; → cacheable resource, was in the cache but has an expired TTL. Served from the origin.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;BYPASS&lt;/code&gt; → cacheable resource, not cached due to directive, cookie, etc …&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;DYNAMIC&lt;/code&gt; → uncacheable resource&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, resources (or URLs) are organized into &lt;em&gt;cacheable&lt;/em&gt; and &lt;em&gt;uncacheable&lt;/em&gt; assets. Cacheable resources have more nuanced statuses, depending on the state of the Cloudflare cache and other settings such as the TTL, the Cookies, etc.&lt;/p&gt;

&lt;p&gt;(Another useful header is the &lt;code&gt;Cf-Ray&lt;/code&gt;, which is a unique identifier of this request. You can use this ID in the CloudFlare console to find out more about the request)&lt;/p&gt;




&lt;h2&gt;
  
  
  What is a “resource”
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;👉 By default, Cloudflare considers URLs with different query parameters as different pages.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the previous section, we talked about &lt;em&gt;resources&lt;/em&gt; and hinted they mapped to URLs. To clarify, the definition of a resource depends on the &lt;strong&gt;Cache Level&lt;/strong&gt; of your Cloudflare zone. The Cache Level can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Basic&lt;/strong&gt;, no query string → only cache when the URL has no query string&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;derlin.ch/main&lt;/code&gt; is cached, &lt;code&gt;derlin.ch/main?foo=bar&lt;/code&gt; isn’t&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Simple&lt;/strong&gt;, ignore query strings → deliver the same resource regardless of query strings&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;derlin.ch/main?ignore=this&lt;/code&gt; is cached and the same resource as &lt;code&gt;derlin.ch/main&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Standard&lt;/strong&gt;, aggressive → deliver a different &lt;em&gt;resource&lt;/em&gt; with each unique query string&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;derlin.ch/main?foo=bar&lt;/code&gt;, &lt;code&gt;derlin.ch/main&lt;/code&gt; and &lt;code&gt;derlin.ch/main?foo=bar&amp;amp;x=1&lt;/code&gt; are all separate resources cached separately&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;The Cache Level is &lt;strong&gt;Standard&lt;/strong&gt; by default, meaning a CloudFlare resource maps 1-1 to a URL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Which resources are eligible for cache
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;👉 Resources eligible for cache must end with a whitelisted extension; JSON and HTML are not cached.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What makes a resource eligible for cache?&lt;/p&gt;

&lt;p&gt;Let’s say you have your portfolio website behind Cloudflare. You would expect your public home page, e.g. &lt;code&gt;https://derlin.ch&lt;/code&gt;, to be cached automatically, right? Nope!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cloudflare only caches based on file extension and not by MIME type.&lt;br&gt;&lt;br&gt;
The Cloudflare CDN &lt;strong&gt;&lt;em&gt;does not cache HTML or JSON by default&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When we say &lt;em&gt;extension&lt;/em&gt;, we talk about the last bit of the URL (think file extensions). The &lt;code&gt;Content-Type&lt;/code&gt; header (MIME type) is never taken into account. Furthermore, Cloudflare has a list of &lt;a href="https://developers.cloudflare.com/cache/concepts/default-cache-behavior/#default-cached-file-extensions" rel="noopener noreferrer"&gt;default cached file extensions&lt;/a&gt; (&lt;code&gt;.jpg&lt;/code&gt;, &lt;code&gt;.css&lt;/code&gt;, &lt;code&gt;.js&lt;/code&gt;, …), and URLs not part of this list are &lt;em&gt;de facto&lt;/em&gt; uncacheable.&lt;/p&gt;

&lt;p&gt;To better understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;derlin.ch/data/cv.pdf&lt;/code&gt; is cacheable (&lt;code&gt;.pdf&lt;/code&gt; is part of the list)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;derlin.ch/github-logo.png&lt;/code&gt; is cacheable (&lt;code&gt;.png&lt;/code&gt; is part of the list)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;derlin.ch/index.html&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; cacheable (&lt;code&gt;.html&lt;/code&gt; is not part of the list)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;derlin.ch&lt;/code&gt; or &lt;code&gt;derlin.ch/main&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; cacheable (no extension)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Which eligible resources are cached
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;👉 Cloudflare strictly respects cache control headers returned by the origin.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once a resource is eligible for cache (cacheable), Cloudflare still needs to decide whether or not to cache it. For this, it &lt;strong&gt;strictly respects cache-control headers&lt;/strong&gt;. In other words, the cache / not cache decision is based on the following information:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control" rel="noopener noreferrer"&gt;&lt;code&gt;Cache-Control&lt;/code&gt;&lt;/a&gt; and &lt;code&gt;Expires&lt;/code&gt;headers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the origin status code&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From the Mozilla documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The HTTP &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control" rel="noopener noreferrer"&gt;&lt;code&gt;Cache-Control&lt;/code&gt;&lt;/a&gt; holds &lt;em&gt;directives&lt;/em&gt; (instructions) in responses (and requests!) that control caching in browsers and shared caches (e.g., Proxies, CDNs).&lt;/p&gt;

&lt;p&gt;The HTTP &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Expires" rel="noopener noreferrer"&gt;&lt;code&gt;Expires&lt;/code&gt;&lt;/a&gt; response header contains the date/time after which the response is considered expired in the context of HTTP caching.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most web frameworks these days come with pre-configured cache-control directives. If your application doesn’t have them, you have work to do!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;QUICK DIGRESSION&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I have to cite the &lt;a href="https://developers.cloudflare.com/cache/concepts/cache-control/" rel="noopener noreferrer"&gt;docs&lt;/a&gt; on this, just because I love the “&lt;em&gt;its an option, but you can’t do anything about it on basically all subscription levels&lt;/em&gt;” 😂:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When enabled on an Enterprise customer's website, it indicates that Cloudflare should strictly respect &lt;code&gt;Cache-Control&lt;/code&gt; directives received from the origin server. Free, Pro, and Business customers have this &lt;strong&gt;option enabled by default&lt;/strong&gt; and cannot disable it.&lt;/p&gt;

&lt;p&gt;Cloudflare's &lt;a href="https://developers.cloudflare.com/cache/how-to/cache-rules/" rel="noopener noreferrer"&gt;Cache Rules&lt;/a&gt; allows users to either augment or override an origin server's &lt;code&gt;Cache-Control&lt;/code&gt; headers or &lt;a href="https://developers.cloudflare.com/cache/concepts/default-cache-behavior/" rel="noopener noreferrer"&gt;default policies&lt;/a&gt; set by Cloudflare.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;When fetching a resource from the origin, Cloudflare looks at the response and applies the following &lt;a href="https://developers.cloudflare.com/cache/concepts/default-cache-behavior/" rel="noopener noreferrer"&gt;default cache behavior&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Cloudflare &lt;strong&gt;does&lt;/strong&gt; cache the resource if one of the following is true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;Cache-Control&lt;/code&gt; header is set to &lt;code&gt;public&lt;/code&gt; and &lt;code&gt;max-age&lt;/code&gt; is greater than 0&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;Expires&lt;/code&gt; header is set to a future date&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(If both&lt;/em&gt; &lt;code&gt;max-age&lt;/code&gt; and an &lt;code&gt;Expires&lt;/code&gt; header are present, Cloudflare uses &lt;code&gt;max-age&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Cloudflare &lt;strong&gt;does not&lt;/strong&gt; cache the resource if one of the following is true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;Cache-Control&lt;/code&gt; header is set to &lt;code&gt;private&lt;/code&gt;, &lt;code&gt;no-store&lt;/code&gt;, &lt;code&gt;no-cache&lt;/code&gt;, or &lt;code&gt;max-age=0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://developers.cloudflare.com/cache/concepts/cache-behavior/#interaction-of-set-cookie-response-header-with-cache" rel="noopener noreferrer"&gt;Set-Cookie header&lt;/a&gt; exists.&lt;/li&gt;
&lt;li&gt;The HTTP request method is anything other than a &lt;code&gt;GET&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;If there is no &lt;code&gt;Cache-Control&lt;/code&gt; or &lt;code&gt;Expires&lt;/code&gt;, CloudFlare caches responses (with varied TTL) only when the origin status code is one of &lt;code&gt;200&lt;/code&gt;, &lt;code&gt;206&lt;/code&gt;, &lt;code&gt;301-303&lt;/code&gt;, &lt;code&gt;404&lt;/code&gt; and &lt;code&gt;410&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;all resources with cacheable extensions will be cached unless you have headers that tell Cloudflare otherwise&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;redirects and 404s will be cached&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;errors such as 500 won’t be cached&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(&lt;strong&gt;NOTE&lt;/strong&gt;: Your zone settings may override this behavior, and there are additional specifics I haven’t covered - e.g. &lt;a href="https://developers.cloudflare.com/cache/concepts/default-cache-behavior/#cacheable-size-limits" rel="noopener noreferrer"&gt;cacheable size limits&lt;/a&gt;, &lt;code&gt;Set-Cookie&lt;/code&gt; handling, and more. When in doubt, always refer to the official documentation.)&lt;/p&gt;




&lt;h2&gt;
  
  
  How to extend cached resources to HTML (and others)
&lt;/h2&gt;

&lt;p&gt;You have understood by now that most of the pages making up your website (HTML, JSON, …) are not even considered for caching - even though they would likely benefit from it!&lt;/p&gt;

&lt;p&gt;You may believe changing this behavior is tricky and requires a deep knowledge of your pages and Cloudflare. What if I tell you it only requires a teeny-tiny generic rule at the right place?&lt;/p&gt;

&lt;p&gt;Indeed, assuming your application is configured to return relevant cache-control headers, the magic comes in one of two forms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;either create a new &lt;em&gt;Page Rule&lt;/em&gt; matching &lt;code&gt;*&lt;/code&gt; and setting &lt;code&gt;Cache-Level: Cache Everything&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feu2i762jaypz2eveg2uv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feu2i762jaypz2eveg2uv.png" alt="Page Rule Cache Everything" width="800" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;or use a &lt;em&gt;Cache Rule&lt;/em&gt; with a similar yield:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffx6yrkgs2j3zvnrp07mu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffx6yrkgs2j3zvnrp07mu.png" alt="Cache Rule Cache Everything" width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Cache Rules are now preferred over Page Rules).&lt;/p&gt;

&lt;p&gt;It may sound weird (especially the page rule), but this will &lt;strong&gt;promote all resources to cacheable&lt;/strong&gt; (a.k.a. eligible for cache). If you followed along, you know that it doesn’t impact the next step of the process: Cloudflare will still use the Cache-Control headers and status code to make the final cache / no cache decision.&lt;/p&gt;

&lt;p&gt;In other words, your admin pages (with a proper &lt;code&gt;Cache-Control: private, no-store, no-cache&lt;/code&gt;) and pages with a session cookie won’t be cached, but now your HTML home page (with a proper &lt;code&gt;Cache-Control: public, max-age=3600&lt;/code&gt;) will!&lt;/p&gt;

&lt;p&gt;Once this rule is on, the number of &lt;code&gt;DYNAMIC&lt;/code&gt; pages (uncacheable) will reduce drastically. You may still need to fine-tune the behaviors, but at least it will provide a decent baseline to work with.&lt;/p&gt;




&lt;p&gt;I was initially confused by Cloudflare’s caching behavior, especially when HTML pages returned a DYNAMIC cache status. The documentation is extensive but overwhelming and I spent quite a few hours on it, trying to figure it out. The redaction of this article helped clarify everything in my head.&lt;/p&gt;

&lt;p&gt;The rule shared in the last section is something we use in production. Without it, the cache gains would be far less tangible.&lt;/p&gt;

&lt;p&gt;I can only hope it helps you too. Don’t hesitate to drop a like or a comment, your reactions always make me smile 🤗.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>webdev</category>
      <category>todayilearned</category>
      <category>caching</category>
    </item>
    <item>
      <title>How to connect to AWS OpenSearch or Elasticsearch clusters using python</title>
      <dc:creator>Lucy Linder</dc:creator>
      <pubDate>Wed, 18 Dec 2024 13:01:00 +0000</pubDate>
      <link>https://dev.to/derlin/how-to-connect-to-aws-opensearch-or-elasticsearch-clusters-using-python-1942</link>
      <guid>https://dev.to/derlin/how-to-connect-to-aws-opensearch-or-elasticsearch-clusters-using-python-1942</guid>
      <description>&lt;p&gt;Connecting to an OpenSearch (ES) service running in AWS using Python is painful. Most examples I find online either don't work or are outdated, leaving me constantly fixing the same issues. To save time and frustration, here’s a collection of working code snippets, up-to-date as of December 2024.&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;Connect using the opensearch-py library (OpenSearch + ElasticSearch)&lt;/li&gt;
&lt;li&gt;
Connect using the elasticsearch library (ElasticSearch only)

&lt;ul&gt;
&lt;li&gt;elasticsearch &amp;gt;= 8&lt;/li&gt;
&lt;li&gt;elasticsearch &amp;lt; 8&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;






&lt;h2&gt;
  
  
  Connect using the opensearch-py library (OpenSearch + ElasticSearch)
&lt;/h2&gt;

&lt;p&gt;This is my preferred way of connecting to an ES instance managed by AWS. It works for both ElasticSearch and OpenSearch clusters, and the authentication can take advantage of AWS profiles.&lt;/p&gt;

&lt;p&gt;Install &lt;code&gt;opensearch-py&lt;/code&gt; and &lt;code&gt;boto3&lt;/code&gt; (for authentication):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;opensearch-py boto3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the time of writing, this installs &lt;code&gt;opensearch-py==2.8.0&lt;/code&gt; and &lt;code&gt;boto3==1.35.81&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now, you can create a client using the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;opensearchpy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;AWSV4SignerAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;OpenSearch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RequestsHttpConnection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;es_host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search-my-aws-esdomain-5k2baneoyj4vywjseocultv2au.eu-central-1.es.amazonaws.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;aws_access_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AKIAXCUEGTAF3CV7GYKA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;aws_secret_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JtA2r/I6BQDcu5rmOK0yISOeJZm58dul+WJeTgK2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eu-central-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Note: you can also use boto3.Session(profile_name="my-profile") or other ways
&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;aws_access_key_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;aws_access_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aws_secret_access_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;aws_secret_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;hosts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;es_host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="n"&gt;http_auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;AWSV4SignerAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_credentials&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;es&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;connection_class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;RequestsHttpConnection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;use_ssl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;boto3.Session&lt;/code&gt; supports various ways of creating a session: using a profile, environment variables, and more. I will let you check it out!&lt;/p&gt;

&lt;p&gt;Once you have it, check the connection using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# should return True
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# use this to get a proper error message if ping fails
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To check indices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# List all indices
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Check the existence of an indice
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Connect using the elasticsearch library (ElasticSearch only)
&lt;/h2&gt;

&lt;p&gt;🔥 This &lt;strong&gt;only works for ElasticSearch clusters&lt;/strong&gt;! Connecting to an OpenSearch cluster raises&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;UnsupportedProductError: The client noticed that the server is not Elasticsearch and we do not support this unknown product&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  elasticsearch &amp;gt;= 8
&lt;/h3&gt;

&lt;p&gt;&lt;small&gt;Most snippets are still referencing &lt;code&gt;RequestsHttpConnection&lt;/code&gt;, a class that was removed in &lt;code&gt;elasticsearch&lt;/code&gt; 8.X. If you were googling for the error &lt;code&gt;cannot import name 'RequestsHttpConnection' from 'elasticsearch’&lt;/code&gt;, you are at the right place!&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Install &lt;code&gt;elasticsearch&lt;/code&gt; (this should install &lt;code&gt;elastic-transport&lt;/code&gt; as well), and &lt;code&gt;requests_aws4auth&lt;/code&gt; . The latter, based on &lt;code&gt;requests&lt;/code&gt;, is required to handle authentication to AWS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;elasticsearch requests-aws4auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the time of writing, this installs &lt;code&gt;elastic-transport==8.15.1&lt;/code&gt;, &lt;code&gt;elasticsearch==8.17.0&lt;/code&gt; and &lt;code&gt;requests-aws4auth==1.3.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now, you can create a client using the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;elastic_transport&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RequestsHttpNode&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;elasticsearch&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Elasticsearch&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;requests_aws4auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AWS4Auth&lt;/span&gt;

&lt;span class="n"&gt;es_endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search-my-aws-esdomain-5k2baneoyj4vywjseocultv2au.eu-central-1.es.amazonaws.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;aws_access_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AKIAXCUEGTAF3CV7GYKA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;aws_secret_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JtA2r/I6BQDcu5rmOK0yISOeJZm58dul+WJeTgK2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eu-central-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;es&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Elasticsearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;es_host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;http_auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;AWS4Auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;aws_access_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;aws_secret_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;es&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;verify_certs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;node_class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;RequestsHttpNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have it, check the connection using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# should return True
&lt;/span&gt;&lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# use this to get a proper error message if ping fails
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  elasticsearch &amp;lt; 8
&lt;/h3&gt;

&lt;p&gt;If you are still on an old version of elasticsearch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;elasticsearch&amp;lt;8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;aws4auth&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Currently &lt;code&gt;elasticsearch==7.17.12&lt;/code&gt;, &lt;code&gt;requests-aws4auth==1.3.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now, you can create a client using the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;elasticsearch&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Elasticsearch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RequestsHttpConnection&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;requests_aws4auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AWS4Auth&lt;/span&gt;

&lt;span class="n"&gt;es_endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search-my-aws-esdomain-5k2baneoyj4vywjseocultv2au.eu-central-1.es.amazonaws.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;aws_access_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AKIAXCUEGTAF3CV7GYKA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;aws_secret_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JtA2r/I6BQDcu5rmOK0yISOeJZm58dul+WJeTgK2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eu-central-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;es&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Elasticsearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;es_endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;http_auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;AWS4Auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;aws_access_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aws_secret_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;es&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;use_ssl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;verify_certs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;connection_class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;RequestsHttpConnection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# should return True
&lt;/span&gt;&lt;span class="n"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# use this to get a proper error message if ping fails
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>python</category>
      <category>aws</category>
      <category>todayisearched</category>
      <category>elasticsearch</category>
    </item>
    <item>
      <title>The Twelve-Factor App: A Blueprint for Scalable, Maintainable Software</title>
      <dc:creator>Lucy Linder</dc:creator>
      <pubDate>Mon, 26 Aug 2024 12:19:09 +0000</pubDate>
      <link>https://dev.to/derlin/the-twelve-factor-app-a-blueprint-for-scalable-maintainable-software-55pn</link>
      <guid>https://dev.to/derlin/the-twelve-factor-app-a-blueprint-for-scalable-maintainable-software-55pn</guid>
      <description>&lt;p&gt;I am working for a PaaS (Platform As a Service), meaning that we not only have our internal apps to maintain, but we manage thousands of client apps that are all different and yet have to run on the same platform.&lt;/p&gt;

&lt;p&gt;How? At &lt;a href="https://divio.com" rel="noopener noreferrer"&gt;Divio&lt;/a&gt;, we strictly follow &lt;a href="https://12factor.net" rel="noopener noreferrer"&gt;the Twelve-Factor App&lt;/a&gt; and have built our platform so that an app adhering to the same standards (plus a few conventions) can be deployed in minutes.&lt;/p&gt;

&lt;p&gt;Initially published by Heroku engineers in 2014, these principles are best practices that guide the design of "Web Apps" to boost flexibility, scalability, maintainability, security, and [... &lt;em&gt;ask ChatGPT for more&lt;/em&gt; ...]. They make it easier to deploy anywhere, especially in the cloud.&lt;/p&gt;

&lt;p&gt;I had trouble understanding the benefits until I practically witnessed the power of those 12 simple best practices. Some I already applied intuitively (without knowing why), and others opened a new way of designing and architecting software. Sticking to those principles is a game-changer, as they simplify and resolve many common problems and pitfalls.&lt;/p&gt;

&lt;p&gt;I've been dying to talk about them for a long time, so without further ado, let's dive in!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(I will stay quite generic in this article, let me know in the comments if you want a follow-up with more practical examples).&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The Twelve-Factor App principles:&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;I Codebase&lt;/li&gt;
&lt;li&gt;II. Dependencies&lt;/li&gt;
&lt;li&gt;III. Config&lt;/li&gt;
&lt;li&gt;IV. Backing services&lt;/li&gt;
&lt;li&gt;V. Build, release, run&lt;/li&gt;
&lt;li&gt;VI. Processes&lt;/li&gt;
&lt;li&gt;VII. Port binding&lt;/li&gt;
&lt;li&gt;VIII. Concurrency&lt;/li&gt;
&lt;li&gt;IX. Disposability&lt;/li&gt;
&lt;li&gt;X. Dev/prod parity&lt;/li&gt;
&lt;li&gt;XI. Logs&lt;/li&gt;
&lt;li&gt;XII. Admin processes&lt;/li&gt;
&lt;/ul&gt;






&lt;h2&gt;
  
  
  I &lt;a href="https://12factor.net/codebase" rel="noopener noreferrer"&gt;Codebase&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;one codebase tracked in revision control, many deploys&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An app should only have a single codebase, and each codebase must be using a version control (git, mercurial, svn). If an app is split into multiple codebases, then it is not an app, it's a distributed system - and the factors should apply to each app composing it.&lt;/p&gt;

&lt;p&gt;"Deploys" refer to running instances of the app. Many deploys mean the same codebase must be used in all environments - in production(s), locally, in QA, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  II. &lt;a href="https://12factor.net/dependencies" rel="noopener noreferrer"&gt;Dependencies&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Explicitly declare and isolate dependencies&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;First, an app should never rely on implicit, transitive or system-wide packages, but instead declare &lt;em&gt;all&lt;/em&gt; its dependencies (including their version and possibly an SHA) using a dependency manifest. Examples are &lt;code&gt;pom.xml&lt;/code&gt; in Java, &lt;code&gt;requirements.txt&lt;/code&gt; or &lt;code&gt;pyproject.toml&lt;/code&gt; in Python, &lt;code&gt;Gemfile&lt;/code&gt; in Ruby, &lt;code&gt;package.json&lt;/code&gt; in Node.&lt;/p&gt;

&lt;p&gt;Second, dependencies should be isolated from the underlying system on which an instance runs. This is typically achieved using tools such as virtual environments in Python and containerization (Docker).&lt;/p&gt;

&lt;p&gt;This factor thus ensures consistency and portability. By making the app's environment predictable and reproducible, it suppresses the "&lt;em&gt;it works on my machine&lt;/em&gt;" conundrum and streamlines the whole flow (onboarding new developers, boosting the stability of build &amp;amp; tests in CI, deploying on new environments, etc).&lt;/p&gt;

&lt;h2&gt;
  
  
  III. &lt;a href="https://12factor.net/config" rel="noopener noreferrer"&gt;Config&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Store config in the environment&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Anything that can vary between deploys should be read from environment variables. Config usually entails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;connection information to backing services (database, caching layer, ...)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;credentials to external services (Facebook, AWS, kubectl, ...)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;other configurations: hostname, size of the thread pool, timeouts, etc.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Strictly separating config from code allows the same codebase to run in widely different environments (locally, in staging or QA, in production(s)). Environment variables are a simple construct supported everywhere that can be managed securely and are easily updated. Thus, the configuration can be adjusted dynamically at any time just by restarting the app. This makes the code flexible, portable, and well-suited for CI/CD. Moreover, it avoids the mistake of committing sensitive information!&lt;/p&gt;

&lt;p&gt;
  Some additional tips on environment variables
  &lt;ul&gt;
&lt;li&gt;&lt;p&gt;Avoid the use of default values when reading configs from the environment. Always be explicit about config in every environments makes it clearer and prevents mistakes. If something is missing, crash early.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Prefer long, descriptive names, and clearly state the type or unit expected. For example, &lt;code&gt;CACHE_READ_TIMEOUT_SECONDS=5&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Group similar configuration using a prefix, not a suffix. This way, you can easily sort through your configurations (&lt;code&gt;env | sort&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simplify local development by taking advantage of &lt;code&gt;.env&lt;/code&gt; files (e.g. a committed &lt;code&gt;base.env&lt;/code&gt; and an uncommitted &lt;code&gt;secrets.env&lt;/code&gt;). If you are using docker, they are easy to source and if you are not, consider tools such as &lt;a href="https://direnv.net" rel="noopener noreferrer"&gt;direnv&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;



&lt;/p&gt;

&lt;h2&gt;
  
  
  IV. &lt;a href="https://12factor.net/backing-services" rel="noopener noreferrer"&gt;Backing services&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Treat backing services as attached resources&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A backing service is any service the app consumes as part of its normal operation. This may include databases, caching systems, SMTP servers, etc. Those services should be accessed solely via the network using config(s) stored in the environment (see III. Config). Here is an example config for a database (DSN stands for Data Source Name): &lt;code&gt;DATABASE_DSN=postgresql://user:password@hostname/db_name&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Treating all backing services the same way, the app doesn't differentiate between local and third-party services. It doesn't matter how the resource is managed or where it is running, as long as it is accessible over the network and complies with the expected protocol. This brings loose coupling and flexibility, as backing services become interchangeable: a database may be swapped from an on-prem instance to an AWS RDS database without any code change, while locally, the app can connect to a database running on Docker.&lt;/p&gt;

&lt;p&gt;
  Design for flexibility, but only if required
  &lt;p&gt;Ideally, your app should settle on one backing service implementation (e.g. PostgreSQL for database). However, if you start having to support multiple implementations of a service, rely as much as possible on standard protocols and connection libraries (e.g. an ORM). If this is not enough, switch drivers at runtime with the strategy and bridge design patterns. Other design patterns (dependency-injection) are also handy!&lt;/p&gt;



&lt;/p&gt;

&lt;p&gt;IMPORTANT: A Twelve-Factor app should be stateless (VI. Processes) with every persistent information stored in backing services. This includes the filesystem! If you use the filesystem for more than temporary operations or static assets loading, consider object storages such as MinIO and S3.&lt;/p&gt;

&lt;h2&gt;
  
  
  V. &lt;a href="https://12factor.net/build-release-run" rel="noopener noreferrer"&gt;Build, release, run&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Strictly separate build and run stages&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The lifecycle of a codebase should follow three separate, non-overlapping steps: build, release, and run, each with a decreasing level of complexity. The build compiles the code into an artifact (e.g. an executable, a docker image). The release adds the configuration for a specific environment. The run starts the instance (or restarts it during a scale or after a crash).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwon6hc0dzzmvc7vyvya.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwon6hc0dzzmvc7vyvya.png" alt="build-deploy-run" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For traceability, each release should be immutable and uniquely identified. This way you can reproduce any version of your application consistently, making debugging, deployment, and scaling easier.&lt;/p&gt;

&lt;p&gt;
  How we do it at Divio
  &lt;p&gt;The way we separate those steps at Divio is a good example, so let me give you the gist.&lt;/p&gt;

&lt;p&gt;At Divio, each environment tracks a specific branch of a git repo and has a settings section for you to configure environment variables. When you click the deploy button, what we do is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;build&lt;/strong&gt;: we clone your repository and create a &lt;em&gt;build image&lt;/em&gt; (the artifact) from your Dockerfile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;release&lt;/strong&gt;: we build a new &lt;em&gt;release image&lt;/em&gt; that inherits from the build image and adds all the environment variables defined in the settings at the time (that is, we build a Dockerfile containing only a &lt;code&gt;FROM build-image&lt;/code&gt; and one or more &lt;code&gt;ENV XXX=YYY&lt;/code&gt; directives). In other words, your current environment variable settings are frozen within the release image. Each release has a unique, incremental build number.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;run&lt;/strong&gt;: we simply start one or more containers from the &lt;em&gt;release image&lt;/em&gt;. Those containers can be scaled and moved around easily as the release image is self-contained!&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;



&lt;/p&gt;

&lt;h2&gt;
  
  
  VI. &lt;a href="https://12factor.net/processes" rel="noopener noreferrer"&gt;Processes&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Execute the app as one or more stateless processes&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An app should consist of one or more Unix-like processes that can be started and stopped anywhere at will. To make this possible, each process must be disposable, strictly stateless, and share nothing.&lt;/p&gt;

&lt;p&gt;In other words, all persistent data must be stored in backing services (IV. Backing services). Memory and filesystems may only be used for transient, temporary, and single-process operations. Processes should behave the same whether they all run on the same machine or in another part of the world.&lt;/p&gt;

&lt;p&gt;This makes the app highly resilient and easily scalable.&lt;/p&gt;

&lt;h2&gt;
  
  
  VII. &lt;a href="https://12factor.net/port-binding" rel="noopener noreferrer"&gt;Port binding&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Export services via port binding&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The app should be entirely self-contained, and expose its functionalities by binding to a port.&lt;/p&gt;

&lt;p&gt;Self-contained stresses an app should not rely on any pre-installed application on the running environment. A web or WSGI application should thus ship with its own server (Apache, Nginx, ...) instead of relying on an external one. This is an extension of II. Dependencies.&lt;/p&gt;

&lt;p&gt;Binding to a port means the app is accessible via a URL (or another locator such as a web socket address) and can itself be turned into a backing service for another app.&lt;/p&gt;

&lt;p&gt;The port is the contract with the execution environment and the app should not care about anything else - such as exposing the port to the outside. It means the app may be accessed directly during development (e.g. &lt;a href="https://localhost:%3Cport%3E/" rel="noopener noreferrer"&gt;https://localhost:&amp;lt;port&amp;gt;/&lt;/a&gt;). At the same time, in production, a &lt;em&gt;routing layer&lt;/em&gt; takes care of mapping a public domain (and handling SSL termination) to the app's exposed port. The app itself should never handle the routing directly - it is the job of the execution environment.&lt;/p&gt;

&lt;p&gt;Note that it doesn't only apply to HTTP/HTTPS: nearly everything can run by listening to a port for incoming requests!&lt;/p&gt;

&lt;h2&gt;
  
  
  VIII. &lt;a href="https://12factor.net/concurrency" rel="noopener noreferrer"&gt;Concurrency&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Scale out via the process model&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Processes are first-class citizens. An app should be designed to handle multiple tasks simultaneously via the process model. Instead of managing multiple threads or processes from within the app, each type of work is assigned to a separate process. Those processes can in turn be moved around and scaled up and down as needed (VI. Processes).&lt;/p&gt;

&lt;p&gt;For example, a typical web application is composed of &lt;em&gt;web&lt;/em&gt; processes to handle the HTTP requests, &lt;em&gt;worker&lt;/em&gt; processes to run long-running background tasks, and &lt;em&gt;cron&lt;/em&gt; processes running scheduled tasks. Processes can even be more specialized: a set of workers for time-sensitive tasks versus low-priority workers.&lt;/p&gt;

&lt;p&gt;This specialization of tasks means each process type can be optimized for its particular function (e.g. more CPU time for high-priority workers or running low-priority workers on cheaper hardware) and can be scaled separately to face any load scenarios.&lt;/p&gt;

&lt;p&gt;The ability to start different kind of processes anywhere (since they are stateless, IV. Processes) makes the app highly resilient, flexible, and especially suited for cloud environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  IX. &lt;a href="https://12factor.net/disposability" rel="noopener noreferrer"&gt;Disposability&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Maximize robustness with fast startup and graceful shutdown&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From IV. Processes and VIII. Concurrency, it follows that processes are disposable resources. As such, they must be able to start quickly and handle both planned and spurious shutdowns gracefully - by leaving the system in a consistent state.&lt;/p&gt;

&lt;p&gt;Graceful shutdown is achieved by exiting properly upon a termination signal. Handling crashes properly is eased by the fact that processes are stateless.&lt;/p&gt;

&lt;p&gt;For example, a worker should either finish or return the current task to the queue upon a termination signal. The queuing system should re-enqueue any unfinished tasks after a given timeout so the sudden death of a worker is not an issue.&lt;/p&gt;

&lt;p&gt;Disposability is the cornerstone that lets you scale processes at will without fear. It guarantees your system stays robust and coherent without losing flexibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  X. &lt;a href="https://12factor.net/dev-prod-parity" rel="noopener noreferrer"&gt;Dev/prod parity&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Keep development, staging, and production as similar as possible&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The gaps between environments should be kept as low as possible. The goal is to minimize the differences in time (how long it takes to deploy a change in production), personnel (people involved in developing and deploying the app), and tools (software, infrastructure/backing services).&lt;/p&gt;

&lt;p&gt;Especially for backing services, ensure you use the same service type and versions in all environments. It may come in different flavors (e.g. a simple docker container running &lt;code&gt;postgres:15&lt;/code&gt; locally, a fully-pledged RDS 15.7 on prod), but the version and protocols should be aligned.&lt;/p&gt;

&lt;p&gt;This principle shares many similarities with the agile and DevOps movements, both of which have demonstrated significant benefits across all aspects of the software lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  XI. &lt;a href="https://12factor.net/logs" rel="noopener noreferrer"&gt;Logs&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Treat logs as event streams&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An app should log information to the standard output (&lt;code&gt;stdout&lt;/code&gt;) as an ephemeral stream and delegate the log management to the execution environment.&lt;/p&gt;

&lt;p&gt;This way, it is up to each execution environment to decide what to do with logs. In production, you may have one or more log aggregators pushing data to external data sources (ElasticSearch, LogStash, Kafka, ...) for monitoring and event detection, while locally, developers can just tail the logs.&lt;/p&gt;

&lt;p&gt;All advanced log management tools can hook to stdout. On the other hand, an app logging to a file requires a filesystem (so &lt;em&gt;not&lt;/em&gt; stateless) and makes it harder to access the logs. In short, do not limit the possibilities by complexifying your code. A simple &lt;code&gt;print&lt;/code&gt; (with timestamps) will do!&lt;/p&gt;

&lt;h2&gt;
  
  
  XII. &lt;a href="https://12factor.net/admin-processes" rel="noopener noreferrer"&gt;Admin processes&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Run admin/management tasks as one-off processes&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We often need to run on-off administrative or maintenance tasks such as migrating data, clearing the cache, re-indexing content, or simply executing arbitrary code to get data or understand the system.&lt;/p&gt;

&lt;p&gt;The Twelve-Factor app recommends always running those tasks &lt;em&gt;against a release&lt;/em&gt;, (V. Build, release, run) using the same codebase and config as the other regular processes. A containerized application makes it easier, as we can spin up a new container alongside the app with the exact same config for one-off purposes.&lt;/p&gt;

&lt;p&gt;To avoid synchronization issues, admin code meant to run more than once should also ideally be part of the codebase.&lt;/p&gt;

&lt;p&gt;For this purpose, frameworks and languages shipping with a REPL (Read Eval Print Loop) shell have a strong advantage. A good example is Django, which provides the &lt;code&gt;./manage.py shell&lt;/code&gt; utility and makes it easy to register custom management commands that run with the same &lt;code&gt;./manage.py&lt;/code&gt; entrypoint (I abuse its power, even in production). Ruby and NodeJS have similar mechanisms.&lt;/p&gt;




&lt;p&gt;The Twelve-Factor, similar to Design Patterns, is here to answer common problems so we do not constantly reinvent the wheel.&lt;/p&gt;

&lt;p&gt;These best practices may seem obtuse, but if you pay attention, you'll notice that many well-designed apps adhere to these principles, knowingly or not. Detect them in the wild and apply them until they become second nature. From my experience, you won't regret it!&lt;/p&gt;

&lt;p&gt;Let me know if you'd like a follow-up article using a project to exemplify those principles and how to put them into use, and happy coding :).&lt;/p&gt;

</description>
      <category>devops</category>
      <category>development</category>
      <category>learning</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Seriously, you need to learn git</title>
      <dc:creator>Lucy Linder</dc:creator>
      <pubDate>Mon, 22 Jul 2024 07:00:00 +0000</pubDate>
      <link>https://dev.to/derlin/seriously-you-need-to-learn-git-1n8j</link>
      <guid>https://dev.to/derlin/seriously-you-need-to-learn-git-1n8j</guid>
      <description>&lt;p&gt;&lt;em&gt;WARNING: ranting incoming.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I have seen countless articles about git (one of the hottest subjects out here), yet I am still puzzled at how few developers actually took the time to &lt;em&gt;learn&lt;/em&gt; git.&lt;/p&gt;




&lt;h2&gt;
  
  
  You do not know git
&lt;/h2&gt;

&lt;p&gt;To clarify what I mean by that, let's play a game. If I ask you to create a feature branch, commit some changes, and make a pull request, I am sure everything is fine. But now, let me vary the game:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;level one&lt;/strong&gt;: you can't use any external tool such as Gitlab or your favorite IDE. No clickops, terminal only. This holds for all the other levels.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;level two&lt;/strong&gt;: you have to split your changes into 5 commits. Bonus: the files are from only two files. And please, don't use the old trick of copying all the changes somewhere, and then pasting them one by one to "prepare" each commit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;level three&lt;/strong&gt;: you pushed your commits 1 to 5, but your reviewer asks to change something in commit 2, reorder commits 3-4, meld commit 5 into 4, change the commit message of commit 1, and rebase your branch to master.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;level four&lt;/strong&gt;: you made a mistake during the level three operation. You need to "recover" and start again, but you already pushed your changes, so a force pull from the origin won't work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;bonus&lt;/strong&gt;: a bug was introduced into the codebase, but you have no idea when. How do you use git to find the commit that started it all?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this is doable when you understand the concepts and inner workings of git - (I wanted to write "&lt;em&gt;when you start actually giving a f**k about the tool you use every day&lt;/em&gt;").&lt;/p&gt;

&lt;p&gt;Why bother? Well, the operations I outlined in the game can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;speed up your development process,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;make your codebase cleaner and your reviews easier: if all the developers in your team learn how to properly split commits and organize them, you can start thinking about conventional commits, automated changelogs, atomic commits, continuous deployment, clean review processes, etc.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;make you fit for open-source: yup, you'll need all that if you want one day to contribute to the Linux kernel or other important codebases!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;save your life when inevitably someone (you?) screws up a git repo&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[... &lt;em&gt;ask ChatGPT to fill up the list&lt;/em&gt; ...]&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don't get me wrong. I am not saying everyone should be a complete master of git, but when 99% of the codebases worldwide are managed through git and "gitting" is a daily activity, I believe spending some time understanding the power of it may be worth it. I myself do not know all the git commands by heart, but I know enough to be aware of the possibilities (and what to google for).&lt;/p&gt;




&lt;h2&gt;
  
  
  But you can start learning
&lt;/h2&gt;

&lt;p&gt;Now that I (I hope!) convinced you, here are some pointers on how to start.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start with the basics
&lt;/h3&gt;

&lt;p&gt;Start by learning git the hard way. Stop using your IDE or git UI for a while, until you feel at ease with all the basic operations directly using the command line. Do not learn by heart, but ask yourself what git is doing under the hood. For example, "staging area", "branches" and "remote" should be crystal clear. You should also understand what's inside the &lt;code&gt;.git/config&lt;/code&gt; , how &lt;code&gt;.gitignore&lt;/code&gt; works, and what are &lt;em&gt;refs&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Here are some of what I consider "basic" commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the obvious &lt;code&gt;clone&lt;/code&gt;, &lt;code&gt;pull&lt;/code&gt;, &lt;code&gt;push&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;remote management: &lt;code&gt;git remote add|remove|show origin&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;git status&lt;/code&gt;, and how to add(remove) files/folders to the staging area (e.g. &lt;code&gt;git rm --cached -r foo/&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;commit (with short and long messages), and especially &lt;em&gt;amend&lt;/em&gt; the last commit (e.g. &lt;code&gt;git commit --amend --reset-author&lt;/code&gt;). Also, ensure you know how to revert the latest commit(s) (&lt;code&gt;git reset --hard/--soft&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;basic branch and tag management: &lt;code&gt;git checkout [-b] ...&lt;/code&gt;, &lt;code&gt;git branch ...&lt;/code&gt;, &lt;code&gt;git tag ...&lt;/code&gt;. Note that checkout works not only with branches but also with files!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;stashing (please, do it for yourself!): &lt;code&gt;git stash&lt;/code&gt; / &lt;code&gt;git stash pop&lt;/code&gt;, ...&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;merge and basic rebase: &lt;code&gt;git merge&lt;/code&gt;, &lt;code&gt;git rebase&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;understand &lt;code&gt;git log&lt;/code&gt;, (or better, &lt;code&gt;git log --graph --format=oneline&lt;/code&gt;) and view a specific ref (&lt;code&gt;git show&lt;/code&gt;, &lt;code&gt;git revparse&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;some other handy commands: &lt;code&gt;git diff&lt;/code&gt;, &lt;code&gt;git revparse&lt;/code&gt;,...&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: this is a very generic advice. When I started programming, my first Java class was not about primitives, it was about &lt;code&gt;java&lt;/code&gt; and &lt;code&gt;javac&lt;/code&gt;. I started with a Notepad and a terminal, and only 4 weeks into the class was I allowed to use an IDE. This was brilliant and stayed with me ever since.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get comfortable with rebasing
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://git-scm.com/docs/git-rebase" rel="noopener noreferrer"&gt;&lt;code&gt;git rebase&lt;/code&gt;&lt;/a&gt; is so powerful it is blinding. You could spend months just playing with it. I personally only know 2 things by heart: (a) how to rebase a branch onto another (e.g. &lt;code&gt;git rebase master&lt;/code&gt; from a feature branch) and (b) how to do interactive rebase.&lt;/p&gt;

&lt;p&gt;Play with interactive rebase yourself, and get familiar with the options: &lt;code&gt;pick&lt;/code&gt;, &lt;code&gt;reword&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, &lt;code&gt;squash&lt;/code&gt;, &lt;code&gt;fixup&lt;/code&gt;. Learn how to handle conflicts during rebase, and how to abort.&lt;/p&gt;

&lt;p&gt;If you fear you'll mess up, don't forget nothing is undoable in git. You can stop an interactive rebase at any point using the &lt;code&gt;git rebase --abort&lt;/code&gt; command. If you already finished the rebase, you still have the reflog ;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment and learn how to undo anything
&lt;/h3&gt;

&lt;p&gt;Trying things out is difficult when you fear about messing up. But anything you do on your local repository can be rolled back. You may have already learned about &lt;code&gt;git reset&lt;/code&gt; and &lt;code&gt;git pull --force&lt;/code&gt;, so the next step is to get acquainted with the reflog.&lt;/p&gt;

&lt;p&gt;Reference logs (&lt;em&gt;reflogs&lt;/em&gt;), keep the history of when the tips of branches and other references in the local repository are modified. For example, when you create/delete a branch, do a rebase, merge something, etc. Each log is associated with an SHA, that you can use to go back in time exactly as you do with commits, using &lt;code&gt;git reset &amp;lt;reflog SHA&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I'll let you figure out the rest by yourself! But now that you are aware of its existence, there is no excuse to try things: merge, rebase, delete, mess up all you want, you're covered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Be curious
&lt;/h3&gt;

&lt;p&gt;When you return to using your IDE / git GUI (tip: try &lt;a href="https://github.com/jesseduffield/lazygit" rel="noopener noreferrer"&gt;lazygit&lt;/a&gt;, it's AMAZING), always ask yourself what command(s) are used in the background.&lt;/p&gt;

&lt;p&gt;Do a &lt;code&gt;git --help&lt;/code&gt; once in a while, and read about the commands you never encountered. For example, have a look at &lt;code&gt;git blame&lt;/code&gt;, &lt;code&gt;git bissect&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;Ask your peers how they use git, and learn from them. Share your learnings and spread the knowledge.&lt;/p&gt;




&lt;h2&gt;
  
  
  Up your game
&lt;/h2&gt;

&lt;p&gt;Now that you're familiar with git, it is time to step up your game. Think of how it could help you streamline your processes and how it can benefit you and your team.&lt;/p&gt;

&lt;p&gt;As a bonus, here are additional advice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Sign&lt;/em&gt; your commits using GPG keys (this will annotate your commits with a nice "verified" badge on GitHub)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Improve your pull requests by following the &lt;a href="https://conventionalcommits.org" rel="noopener noreferrer"&gt;conventional commits&lt;/a&gt; convention, and your reviews by following the &lt;a href="https://conventionalcomments.org/" rel="noopener noreferrer"&gt;conventional comments&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep a clean git history using squashing, and think about adopting &lt;a href="https://dev.to/samuelfaure/how-atomic-git-commits-dramatically-increased-my-productivity-and-will-increase-yours-too-4a84"&gt;atomic commits&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>git</category>
      <category>learning</category>
      <category>beginners</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Idempotency in 256 characters or less</title>
      <dc:creator>Lucy Linder</dc:creator>
      <pubDate>Sun, 23 Jun 2024 14:52:48 +0000</pubDate>
      <link>https://dev.to/derlin/idempotency-in-256-characters-or-less-118c</link>
      <guid>https://dev.to/derlin/idempotency-in-256-characters-or-less-118c</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/cs"&gt;DEV Computer Science Challenge v24.06.12: One Byte Explainer&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Explainer
&lt;/h2&gt;

&lt;p&gt;Borrowed from Mathematics, an &lt;strong&gt;idempotent&lt;/strong&gt; operation can run multiple times without changing the result. Writing to a file is idempotent, but appending to a file is not. A script using idempotent operations can thus fail midway and be restarted safely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Context
&lt;/h2&gt;

&lt;p&gt;This idempotency concept is so important, yet too often unknown. It is the magic behind famous tools such as Terraform, Ansible, Kubernetes Manifests, etc. Fault-tolerant REST APIs are mostly &lt;a href="https://restfulapi.net/idempotent-rest-apis/"&gt;idempotent REST APIs&lt;/a&gt;. It is everywhere, yet cleverly hidden. It is what makes operations repeatable and consistent.&lt;/p&gt;

&lt;p&gt;There are better explanations online, but if my submission makes at least one person curious about this concept and want to know more, then I won already 🙂 !&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>cschallenge</category>
      <category>computerscience</category>
      <category>beginners</category>
    </item>
    <item>
      <title>From Jar to Brew: distribute your Java programs easily with Homebrew and GitHub Actions</title>
      <dc:creator>Lucy Linder</dc:creator>
      <pubDate>Mon, 20 May 2024 12:10:00 +0000</pubDate>
      <link>https://dev.to/derlin/from-jar-to-brew-distribute-your-java-programs-easily-with-homebrew-and-github-actions-p18</link>
      <guid>https://dev.to/derlin/from-jar-to-brew-distribute-your-java-programs-easily-with-homebrew-and-github-actions-p18</guid>
      <description>&lt;p&gt;In this article, we'll explore how to make a program packaged as a Jar (Java, Kotlin, Scala, ...) easily installable by end users using homebrew and GitHub actions.&lt;/p&gt;

&lt;p&gt;This is inspired by my work to distribute &lt;a href="https://github.com/derlin/bitdowntoc"&gt;bitdowntoc&lt;/a&gt; with brew and aims at answering two questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;how to create a homebrew tap to distribute a jar with brew?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;how to automatically update the tap when a new version is available (using GitHub and GitHub actions)?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's get started!&lt;/p&gt;






&lt;ul&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;
Publish your JAR via homebrew

&lt;ul&gt;
&lt;li&gt;Step 1: create a fat jar&lt;/li&gt;
&lt;li&gt;Step 2: make your jar available from a public URL&lt;/li&gt;
&lt;li&gt;Step 3: create a homebrew tap&lt;/li&gt;
&lt;li&gt;Step 4: write the formula&lt;/li&gt;
&lt;li&gt;Step 5: validate and test the formula&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
Keep the homebrew tap in sync

&lt;ul&gt;
&lt;li&gt;Step 1: the tap workflow&lt;/li&gt;
&lt;li&gt;Step 2: the app workflow&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;small&gt;(TOC generated with &lt;a href="https://github.com/derlin/bitdowntoc"&gt;bitdowntoc&lt;/a&gt;)&lt;/small&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;you have standalone software written in a JVM-compatible language (Java, Scala, Kotlin, ...) that you want to distribute easily.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;you are hosting it on GitHub. For the second part, you use GitHub releases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;you have homebrew installed and roughly know what it is about.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, I will use &lt;a href="https://github.com/derlin/bitdowntoc"&gt;bitdowntoc&lt;/a&gt;, a command line (and online) tool to add a table of contents to your Markdown files. Written in Kotlin, it is available &lt;a href="https://derlin.github.io/bitdowntoc"&gt;on the web&lt;/a&gt; and in the command line (using a &lt;a href="https://github.com/derlin/bitdowntoc/releases/latest"&gt;jar&lt;/a&gt; or a &lt;a href="https://github.com/derlin/bitdowntoc/releases/latest"&gt;native executable&lt;/a&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  Publish your JAR via homebrew
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: create a fat jar
&lt;/h3&gt;

&lt;p&gt;The best way to ship JVM-compatible software to all architectures and OSes is through a fat jar. The advantage of a &lt;em&gt;jar&lt;/em&gt; (Java ARchive) is it only requires a JRE (Java Runtime Environment) on the target machine to execute. The colloquial term "fat jar" designates a self-contained jar - an archive that includes all its necessary dependencies.&lt;/p&gt;

&lt;p&gt;Depending on your language and build system, there are many ways to create a fat jar - too much to enumerate here. If you use Java + Gradle (groovy), baeldung's article "&lt;a href="https://www.baeldung.com/gradle-fat-jar"&gt;Creating a Far Jar in Gradle&lt;/a&gt;" is a good start.&lt;/p&gt;

&lt;p&gt;🔥 Ensure your app follows &lt;em&gt;semver&lt;/em&gt; (semantic versioning) and the jar is called &lt;code&gt;&amp;lt;app&amp;gt;-&amp;lt;version&amp;gt;.jar&lt;/code&gt;, where &lt;code&gt;&amp;lt;version&amp;gt;&lt;/code&gt; matches &lt;code&gt;&amp;lt;major&amp;gt;.&amp;lt;minor&amp;gt;.&amp;lt;patch&amp;gt;&lt;/code&gt;. This should be the default when using common tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: make your jar available from a public URL
&lt;/h3&gt;

&lt;p&gt;If you are using GitHub for release management, this means attaching your jar to each release. This can be done manually, or completely automated using GitHub actions.&lt;/p&gt;

&lt;p&gt;bitdowntoc uses &lt;a href="https://github.com/google-github-actions/release-please-action"&gt;release-please&lt;/a&gt; for release automation, and &lt;a href="https://github.com/softprops/action-gh-release"&gt;action-gh-release&lt;/a&gt; to attach build artifacts to the release. For more details, check out the &lt;code&gt;release-please&lt;/code&gt; and &lt;code&gt;upload-jar&lt;/code&gt; in the &lt;a href="https://github.com/derlin/bitdowntoc/blob/main/.github/workflows/release.yml"&gt;release.yml&lt;/a&gt; workflow (I should write an article on release automation 😄).&lt;/p&gt;

&lt;p&gt;However you do it, ensure you can download a properly named jar from a public URL. Here is the URL for bitdowntoc version 2.1.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;https://github.com/derlin/bitdowntoc/releases/download/v2.1.0/bitdowntoc-jvm-2.1.0.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: create a homebrew tap
&lt;/h3&gt;

&lt;p&gt;Homebrew supports formulae and casks. &lt;em&gt;Formulae&lt;/em&gt; build from upstream sources, while &lt;em&gt;casks&lt;/em&gt; install macOS native applications. We could argue a JAR is a native application, but it still requires some fiddling during installation, so we'll use a formula.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;taps&lt;/em&gt; are git repositories of formulae and casks that homebrew uses for discovery. When installing homebrew, it comes with some built-in taps such as &lt;a href="https://github.com/Homebrew/homebrew-core"&gt;homebrew-core&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Adding your software to built-in taps requires it to be "&lt;em&gt;notable enough&lt;/em&gt;" (at least 30 forks, 30 watchers, and 75 stars). If it is your case, go ahead and create a pull request by following the &lt;a href="https://docs.brew.sh/Formula-Cookbook#basic-instructions"&gt;Formula Cookbook&lt;/a&gt;! If like me you lack exposure, you'll have to create and maintain your own tap.&lt;/p&gt;

&lt;p&gt;👉 The following steps are a condensed version of brew's &lt;a href="https://docs.brew.sh/How-to-Create-and-Maintain-a-Tap"&gt;How to Create and Maintain a Tap&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Brew is very developer-friendly. To bootstrap a new tap, run the following in a terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;GH_USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;derlin
&lt;span class="nv"&gt;APP_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bitdowntoc

brew tap-new &lt;span class="nv"&gt;$GH_USERNAME&lt;/span&gt;/&lt;span class="nv"&gt;$APP_NAME&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;brew &lt;span class="nt"&gt;--repository&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/Library/Taps/&lt;span class="nv"&gt;$GH_USERNAME&lt;/span&gt;/homebrew-&lt;span class="nv"&gt;$APP_NAME&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;derlin&lt;/code&gt; with your GitHub username, and &lt;code&gt;bitdowntoc&lt;/code&gt; with the name of your application. This will initialize a git repository called &lt;code&gt;homebrew-&amp;lt;appname&amp;gt;&lt;/code&gt; at the path of the taps on your system with a &lt;code&gt;README.md&lt;/code&gt; and a &lt;code&gt;Formula&lt;/code&gt; directory. Cd into it using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;brew &lt;span class="nt"&gt;--repository&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/Library/Taps/derlin/homebrew-bitdowntoc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tap is created, let's write the formula.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: write the formula
&lt;/h3&gt;

&lt;p&gt;In the tap repository created above, add a ruby file in the &lt;code&gt;Formula&lt;/code&gt; repository. Here is the bitdowntoc formula, that you can adapt to your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;class Bitdowntoc &amp;lt; Formula
  &lt;span class="c"&gt;# ↓↓ ①&lt;/span&gt;
  desc &lt;span class="s2"&gt;"Markdown Table Of Content (TOC) generator"&lt;/span&gt;
  homepage &lt;span class="s2"&gt;"https://derlin.github.io/bitdowntoc"&lt;/span&gt;
  url &lt;span class="s2"&gt;"https://github.com/derlin/bitdowntoc/releases/download/v2.1.0/bitdowntoc-jvm-2.1.0.jar"&lt;/span&gt;
  sha256 &lt;span class="s2"&gt;"b7a21d2a793a2ecb6c20f27aa8fffc72702daa364b2a89bed32580e302194650"&lt;/span&gt;
  license &lt;span class="s2"&gt;"Apache-2.0"&lt;/span&gt;

  &lt;span class="c"&gt;## ↓↓ ②&lt;/span&gt;
  depends_on &lt;span class="s2"&gt;"openjdk"&lt;/span&gt;

  &lt;span class="c"&gt;## ↓↓ ③&lt;/span&gt;
  def &lt;span class="nb"&gt;install
    &lt;/span&gt;libexec.install &lt;span class="s2"&gt;"bitdowntoc-jvm-#{version}.jar"&lt;/span&gt;
    bin.write_jar_script libexec/&lt;span class="s2"&gt;"bitdowntoc-jvm-#{version}.jar"&lt;/span&gt;, &lt;span class="s2"&gt;"bitdowntoc"&lt;/span&gt;
  end

  &lt;span class="c"&gt;## ↓↓ ④&lt;/span&gt;
  &lt;span class="nb"&gt;test &lt;/span&gt;&lt;span class="k"&gt;do
    &lt;/span&gt;assert_match &lt;span class="s2"&gt;"BitDownToc Version: #{version}"&lt;/span&gt;, shell_output&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"#{bin}/bitdowntoc --version"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  end
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are 4 parts to this Ruby file.&lt;/p&gt;

&lt;p&gt;Section ① defines the metadata and the "source" URL (the jar). To get the correct sha, use &lt;code&gt;sha256sum&lt;/code&gt; on Linux or &lt;code&gt;shasum -a 256&lt;/code&gt; on Mac. Ensure your description is less than 80 characters.&lt;/p&gt;

&lt;p&gt;Section ② specifies that the formula requires Java to be installed.&lt;/p&gt;

&lt;p&gt;Section ③ copies the jar (downloaded and unpacked automatically by homebrew using the URL in ①) in the &lt;code&gt;libexec&lt;/code&gt; folder and creates a bash executable to launch it in the &lt;code&gt;bin&lt;/code&gt; folder. The &lt;code&gt;write_jar_script&lt;/code&gt; method takes care of resolving to the proper JDK. In my system, for instance, the bin script looks like this after install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /opt/homebrew/Cellar/bitdowntoc/2.1.0/bin/bitdowntoc
&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;JAVA_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;JAVA_HOME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;JAVA_HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/bin/java"&lt;/span&gt;  &lt;span class="nt"&gt;-jar&lt;/span&gt; &lt;span class="s2"&gt;"/opt/homebrew/Cellar/bitdowntoc/2.1.0/libexec/bitdowntoc-jvm-2.1.0.jar"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Section ④ is completely optional and defines some tests to check the installation worked properly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: validate and test the formula
&lt;/h3&gt;

&lt;p&gt;Now, let's install the formula locally (note that it will download the world if you don't have openjdk already installed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;HOMEBREW_NO_INSTALL_FROM_API&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 brew &lt;span class="nb"&gt;install &lt;/span&gt;bitdowntoc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this works, the next steps are to run the tests and audit the formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;test &lt;/span&gt;bitdowntoc
brew audit &lt;span class="nt"&gt;--strict&lt;/span&gt; bitdowntoc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last step is to commit and push everything to GitHub. Once done, anyone can install your software using either:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap derlin/bitdowntoc
brew &lt;span class="nb"&gt;install &lt;/span&gt;bitdowntoc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or in one line (will fetch the tap and run the install in one command):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;derlin/bitdowntoc/bitdowntoc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Keep the homebrew tap in sync
&lt;/h2&gt;

&lt;p&gt;The homebrew tap lives in a different repository and must be updated every time a new version is available. In the age of the "&lt;em&gt;*** as Code&lt;/em&gt;", this manual step is frowned upon. This section shows how I automated it for bitdowntoc.&lt;/p&gt;

&lt;p&gt;⚠ Note that this assumes you use GitHub Releases and have a GitHub Actions release workflow.&lt;/p&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;create a workflow on the homebrew tap repository able to check for new versions of the app and update the formula accordingly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;trigger this workflow from the release workflow of the app.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;(Note that we could also schedule 1 to run e.g. every week, but this adds latency - imagine you release on Monday but the workflow runs on Sunday! GitHub may also disable scheduled workflows e.g. due to a lack of activity in the repo. So meh.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: the tap workflow
&lt;/h3&gt;

&lt;p&gt;First, let's create a workflow on the homebrew tap repository. Its role is to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;check the latest version available upstream (the main repository),&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;if a new version is available, update the formula and commit the changes.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is what it looks like for bitdowntoc (&lt;a href="https://github.com/derlin/homebrew-bitdowntoc/blob/main/.github/workflows/update.yml"&gt;homebrew-bitdowntoc/.github/workflows/update.yml&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;update version from upstream&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Allow manual triggers&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Necessary to commit the changes back&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;update_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.ref }}&lt;/span&gt;

      &lt;span class="c1"&gt;# ↓↓ ①&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;get upstream version&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;upstream&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;res=$(curl -s https://api.github.com/repos/derlin/bitdowntoc/releases/latest)&lt;/span&gt;
          &lt;span class="s"&gt;url=$(jq -r '.assets[] | select( .content_type == "application/java-archive" ) | .browser_download_url' &amp;lt;&amp;lt;&amp;lt; "$res")&lt;/span&gt;
          &lt;span class="s"&gt;version=$(echo $url | sed -E 's/.*-([0-9]\.[0-9]\.[0-9]).*/\1/')&lt;/span&gt;

          &lt;span class="s"&gt;curl -L -o /tmp/bitdowntoc.jar $url&lt;/span&gt;
          &lt;span class="s"&gt;sha=$(sha256sum /tmp/bitdowntoc.jar | cut -d' ' -f1)&lt;/span&gt;

          &lt;span class="s"&gt;echo "✅ Found version $version: $url" | tee -a $GITHUB_STEP_SUMMARY&lt;/span&gt;
          &lt;span class="s"&gt;echo "url=$url" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
          &lt;span class="s"&gt;echo "version=$version" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
          &lt;span class="s"&gt;echo "sha=$sha" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;

      &lt;span class="c1"&gt;## ↓↓ ②&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;update bitdowntoc formula&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;update&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;formula="Formula/bitdowntoc.rb"&lt;/span&gt;
          &lt;span class="s"&gt;sed -i'' -E 's#^  url ".*"$#  url "${{ steps.upstream.outputs.url }}"#' $formula&lt;/span&gt;
          &lt;span class="s"&gt;sed -i'' -E 's#^  sha256 ".*"$#  sha256 "${{ steps.upstream.outputs.sha }}"#' $formula&lt;/span&gt;

          &lt;span class="s"&gt;if [[ -z "$(git status -s $formula)" ]]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "✅ Already up-to-date" | tee -a $GITHUB_STEP_SUMMARY&lt;/span&gt;
            &lt;span class="s"&gt;echo "changed=" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;echo "🟠 Updated $formula" | tee -a $GITHUB_STEP_SUMMARY&lt;/span&gt;
            &lt;span class="s"&gt;git add $formula&lt;/span&gt;
            &lt;span class="s"&gt;cat $formula&lt;/span&gt;
            &lt;span class="s"&gt;echo "changed=true" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="c1"&gt;## ↓↓ ③&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;commit changes&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.update.outputs.changed == 'true'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git config --global user.name 'Github Workflow'&lt;/span&gt;
          &lt;span class="s"&gt;git config --global user.email 'workflow@bot.github.com'&lt;/span&gt;
          &lt;span class="s"&gt;git commit -am "chore: update bitdowntoc to ${{ steps.upstream.outputs.version }}"&lt;/span&gt;
          &lt;span class="s"&gt;git push&lt;/span&gt;

          &lt;span class="s"&gt;echo "✅ Changes committed" | tee -a $GITHUB_STEP_SUMMARY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It may seem daunting, but this boils down to some basic bash. Let's break it down together.&lt;/p&gt;

&lt;p&gt;In ①, I use the GitHub API to fetch the list of artifacts from the latest GitHub release of bitdowntoc, and find the URL of the one with a &lt;code&gt;application/java-archive&lt;/code&gt; type. Next, I extract the version from the URL (looking for a &lt;code&gt;X.X.X&lt;/code&gt; pattern), download the jar, and compute its SHA. The URL, SHA, and version are stored as a GitHub output of this step.&lt;/p&gt;

&lt;p&gt;In ②, I update the &lt;code&gt;Formula/bitdowntoc.rb&lt;/code&gt; file with the URL and SHA of step ① using a regex (&lt;code&gt;sed&lt;/code&gt;). I can now ask &lt;code&gt;git&lt;/code&gt; if there is any change, and store the result as an output of this step.&lt;/p&gt;

&lt;p&gt;Step ③ only runs if a change was detected in step ②. I configure the git user and commit the changes back to the repo.&lt;/p&gt;

&lt;p&gt;This workflow is &lt;em&gt;idempotent&lt;/em&gt;: I can run it as often as I want, it will only update the tap if a new release is present upstream.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: the app workflow
&lt;/h3&gt;

&lt;p&gt;The last step is to trigger the tap workflow on every release of bitdowntoc. This means adding a new job to my release workflow that uses the GitHub API to trigger the tap workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;update_homebrew&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;release-please&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;upload-jar&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trigger homebrew update&lt;/span&gt;
        &lt;span class="c1"&gt;# The PAT should have actions:read-write&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -L \&lt;/span&gt;
            &lt;span class="s"&gt;-X POST \&lt;/span&gt;
            &lt;span class="s"&gt;--fail-with-body \&lt;/span&gt;
            &lt;span class="s"&gt;-H "Accept: application/vnd.github+json" \&lt;/span&gt;
            &lt;span class="s"&gt;-H "Authorization: Bearer ${{ secrets.HOMEBREW_PAT }}" \&lt;/span&gt;
            &lt;span class="s"&gt;-H "X-GitHub-Api-Version: 2022-11-28" \&lt;/span&gt;
            &lt;span class="s"&gt;https://api.github.com/repos/derlin/homebrew-bitdowntoc/actions/workflows/${{ secrets.HOMEBREW_WORKFLOW_ID }}/dispatches \&lt;/span&gt;
            &lt;span class="s"&gt;-d '{"ref":"main","inputs":{}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the trigger to work, I had to create a PAT (Personal Access Token) with write permissions on the tap repository and add it as a secret (&lt;code&gt;HOMEBREW_PAT&lt;/code&gt;) on the bitdowntoc repository (&lt;em&gt;Settings&lt;/em&gt; &amp;gt; &lt;em&gt;Secrets and variables&lt;/em&gt; &amp;gt; &lt;em&gt;Actions&lt;/em&gt;: &lt;em&gt;Repository secrets&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;needs&lt;/code&gt; here is specific to my workflow, and is necessary to ensure the GitHub release exists (with the jar!) &lt;em&gt;before&lt;/em&gt; the job runs.&lt;/p&gt;

&lt;p&gt;And that's it!&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I always loved homebrew (as a user), but never tried to publish anything. I was pleasantly surprised by the overall architecture and the great documentation. Setting up a personal tap was a breeze. My only reproach is the whole brew-related &lt;a href="https://docs.brew.sh/Formula-Cookbook#homebrew-terminology"&gt;terminology&lt;/a&gt;... For a non-native speaker, &lt;em&gt;cask&lt;/em&gt;, &lt;em&gt;tap&lt;/em&gt;, &lt;em&gt;keg&lt;/em&gt;, &lt;em&gt;cellar&lt;/em&gt;, etc are not straightforward without a dictionary close by.&lt;/p&gt;

&lt;p&gt;The syncing of the tap was more "experimental", but I am quite happy with the result. If you are a tap maintainer, let me know how &lt;em&gt;you&lt;/em&gt; solved this challenge!&lt;/p&gt;

&lt;p&gt;I hope this will be useful to some, and remember, &lt;a href="https://github.com/derlin/bitdowntoc/"&gt;bitdowntoc&lt;/a&gt; can now be installed with homebrew, so give it a try 😉.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>java</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>github</category>
    </item>
    <item>
      <title>How to mock an API in 2 minutes</title>
      <dc:creator>Lucy Linder</dc:creator>
      <pubDate>Thu, 09 May 2024 12:43:00 +0000</pubDate>
      <link>https://dev.to/derlin/how-to-mock-an-api-in-2-minutes-54kc</link>
      <guid>https://dev.to/derlin/how-to-mock-an-api-in-2-minutes-54kc</guid>
      <description>&lt;p&gt;I don't need to enumerate all the situations in which you would need to mock an API or an HTTP server. There are many options available, such as &lt;a href="https://github.com/dotronglong/faker"&gt;faker&lt;/a&gt;. This article delves into &lt;a href="https://www.mock-server.com"&gt;MockServer&lt;/a&gt; and shows how to set up a mock server using only Docker and a JSON file. Let's get started!&lt;/p&gt;

&lt;p&gt;🤯 🔥 MockServer can also be used as a &lt;a href="https://www.mock-server.com/mock_server/getting_started.html"&gt;proxy&lt;/a&gt; (to record and potentially modify requests/responses), or a combination of mock and proxy!&lt;/p&gt;






&lt;ul&gt;
&lt;li&gt;The docker-compose&lt;/li&gt;
&lt;li&gt;
Defining the expectations (spec.json)

&lt;ul&gt;
&lt;li&gt;The most basic expectation&lt;/li&gt;
&lt;li&gt;Using templates&lt;/li&gt;
&lt;li&gt;Importing the request spec from OpenAPI&lt;/li&gt;
&lt;li&gt;Going further&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;






&lt;h2&gt;
  
  
  The docker-compose
&lt;/h2&gt;

&lt;p&gt;First, let's create a docker-compose for running the latest version of MockServer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mockserver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mockserver/mockserver:latest&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1080:1080"&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;MOCKSERVER_PROPERTY_FILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/config/mockserver.properties&lt;/span&gt;
        &lt;span class="na"&gt;MOCKSERVER_INITIALIZATION_JSON_PATH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/config/spec.json&lt;/span&gt;
        &lt;span class="c1"&gt;# ↓ hot reload when the spec.json changes 😍&lt;/span&gt;
        &lt;span class="na"&gt;MOCKSERVER_WATCH_INITIALIZATION_JSON&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
      &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# bind the config for easy editing&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config:/config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing special here. We are using a volume for the config and telling MockServer to load the endpoints to mock from the &lt;code&gt;./config/spec.json&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Before starting it, let's create the necessary files (we will see how to use them later):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;config
&lt;span class="nb"&gt;touch &lt;/span&gt;config/mockserver.properties
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'[]'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; config/spec.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start the mock server by executing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MockServer is now running on &lt;a href="http://localhost:1080"&gt;http://localhost:1080&lt;/a&gt;! However, since the &lt;code&gt;spec.json&lt;/code&gt; - defining the expectations, or endpoints to mock - is empty, it only returns 404's. Let's change that!&lt;/p&gt;

&lt;p&gt;💡 the &lt;code&gt;mockserver.properties&lt;/code&gt; will stay empty for this article, but I recommend you check out the &lt;a href="https://www.mock-server.com/mock_server/configuration_properties.html"&gt;configuration documentation&lt;/a&gt; for all the possibilities it offers! Dynamic SSL certificates, logging, metrics, etc.&lt;/p&gt;




&lt;h2&gt;
  
  
  Defining the expectations (spec.json)
&lt;/h2&gt;

&lt;p&gt;🔥 &lt;em&gt;More examples available at&lt;/em&gt; &lt;a href="https://github.com/mock-server/mockserver/blob/master/mockserver-examples/json_examples.md"&gt;&lt;em&gt;https://github.com/mock-server/mockserver/blob/master/mockserver-examples/json_examples.md&lt;/em&gt;&lt;/a&gt; 🔥&lt;/p&gt;

&lt;h3&gt;
  
  
  The most basic expectation
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;spec.json&lt;/code&gt; file defines &lt;em&gt;expectations&lt;/em&gt;, aka the endpoints to mock and what to return. It contains an object or an array of objects defining the HTTP request to match, and the response to return.&lt;/p&gt;

&lt;p&gt;Unclear? Here is a basic example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"httpRequest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/hello"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"httpResponse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;world&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the &lt;code&gt;spec.json&lt;/code&gt;, wait a second and try hitting &lt;a href="http://localhost:1080/hello"&gt;http://localhost:1080/hello&lt;/a&gt;. You should see the body &lt;code&gt;{"hello": "world"}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is just a simple example, but we can go way deeper. The request may define headers, method, path and query parameters, authentication, and more, all using either string or regex matching. MockServer even supports stuff such as adding delay in the response or behaving differently depending on how often the endpoint is called!&lt;/p&gt;

&lt;h3&gt;
  
  
  Using templates
&lt;/h3&gt;

&lt;p&gt;The response often depends on the request, and enumerating all the possible input/output pairs is tedious. MockServer supports loops, conditionals, and 3 &lt;a href="https://www.mock-server.com/mock_server/response_templates.html"&gt;response template&lt;/a&gt; engines: Javascript, mustache, and velocity.&lt;/p&gt;

&lt;p&gt;Let's see a velocity example &lt;em&gt;(at the time of writing, Javascript is broken in Docker, see the &lt;a href="https://github.com/mock-server/mockserver/issues/1326"&gt;related issue&lt;/a&gt;)&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"httpRequest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/cart/item/{id}/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"pathParameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;d+"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"httpResponseTemplate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"templateType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VELOCITY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"template"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"#set($id = $!request.pathParameters['id'][0]) #if ($id == 1) {&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;statusCode&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 500} #else {&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;statusCode&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 200, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;body&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;{&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;foo&lt;/span&gt;&lt;span class="se"&gt;\\\"&lt;/span&gt;&lt;span class="s2"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;} #end"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What is happening? Instead of specifying the &lt;code&gt;httpResponse&lt;/code&gt; directly, we ask MockServer to "compute" it by interpreting a template. In the example above, the server returns a &lt;code&gt;500&lt;/code&gt; for the item &lt;code&gt;1&lt;/code&gt; and a JSON body otherwise.&lt;/p&gt;

&lt;p&gt;Things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;#set&lt;/code&gt;, &lt;code&gt;#if&lt;/code&gt;/&lt;code&gt;#end&lt;/code&gt;, &lt;code&gt;#foreach&lt;/code&gt;/&lt;code&gt;#end&lt;/code&gt;, etc. are MockServer constructs independent of the template type, allowing for complex logic inside the response.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;$id&lt;/code&gt; is a MockServer local variable, but &lt;code&gt;$!request&lt;/code&gt; (notice the &lt;code&gt;!&lt;/code&gt;) is a velocity template directive.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;there is full support for regular expressions in all request attributes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Request paths and query parameters can be named and accessed in the response. The index &lt;code&gt;[0]&lt;/code&gt; is necessary because MockServer uses Java multivalued maps under the hood. Here, using the request path &lt;code&gt;/cart/item/\\d+/&lt;/code&gt; and the condition &lt;code&gt;($request.path == '/cart/item/1/')&lt;/code&gt; would have been simpler.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Since the template itself is a JSON string, multiple escapes of &lt;code&gt;"&lt;/code&gt; are necessary and no wrap is possible, making it difficult to read... The joys of JSON 😆.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's check the result of the above template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜ curl &lt;span class="nt"&gt;--fail-with-body&lt;/span&gt; http://localhost:1080/cart/item/13/
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"foo"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
➜ curl &lt;span class="nt"&gt;--fail-with-body&lt;/span&gt; http://localhost:1080/cart/item/1/
curl: &lt;span class="o"&gt;(&lt;/span&gt;22&lt;span class="o"&gt;)&lt;/span&gt; The requested URL returned error: 500
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Request attributes (headers, body, path, remote address, etc.) aside, other magic variables are at our disposal in templates. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;$!now_epoch&lt;/code&gt; → get the current timestamp seconds since January 1, 1970 (UNIX epoch)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;$!uuid&lt;/code&gt; → get a random UUID&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;$!rand_int_100&lt;/code&gt; → get a random number between 0 and 99&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;...&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check the &lt;a href="https://www.mock-server.com/mock_server/response_templates.html"&gt;docs&lt;/a&gt; for more!&lt;/p&gt;

&lt;h3&gt;
  
  
  Importing the request spec from OpenAPI
&lt;/h3&gt;

&lt;p&gt;Often, you want to simulate existing endpoints from a known API. If the latter has an &lt;a href="https://www.openapis.org"&gt;OpenAPI&lt;/a&gt; schema, MockServer can parse it to construct the request matchers.&lt;/p&gt;

&lt;p&gt;Let's use the dev.to API as an example, and simulate the &lt;code&gt;GET /api/articles&lt;/code&gt; endpoint: &lt;a href="https://developers.forem.com/api/v1#tag/articles/operation/getArticles"&gt;https://developers.forem.com/api/v1#tag/articles/operation/getArticles&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"httpRequest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"specUrlOrPayload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://developers.forem.com/redocusaurus/plugin-redoc-1.yaml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"operationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"getArticles"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"httpResponse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;super article&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}]"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two lines of the &lt;code&gt;httpRequest&lt;/code&gt; above transform into a 93-line long request matcher. Here is an abridged version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/articles"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"api-key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;".+"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"queryStringParameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"keyMatchStyle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MATCHING_KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"?username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"parameterStyle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FORM_EXPLODED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;}}]},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"?top"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"parameterStyle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FORM_EXPLODED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"int32"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minimum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;}}]},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When importing from OpenAPI, the request matchers are way more precise. For instance, the dev.to schema requires an authentication header. Without it in a request, there is no match, and the MockServer returns a 404. It doesn't complain about invalid query parameters though, which is expected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Missing the authorization header&lt;/span&gt;
➜ curl &lt;span class="nt"&gt;--fail-with-body&lt;/span&gt; http://localhost:1080/api/articles
curl: &lt;span class="o"&gt;(&lt;/span&gt;22&lt;span class="o"&gt;)&lt;/span&gt; The requested URL returned error: 404
&lt;span class="c"&gt;# ok&lt;/span&gt;
➜ curl &lt;span class="nt"&gt;--fail-with-body&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'api-key: x'&lt;/span&gt; http://localhost:1080/api/articles
&lt;span class="o"&gt;[{&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"super article"&lt;/span&gt;&lt;span class="o"&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Going further
&lt;/h3&gt;

&lt;p&gt;To see more, check out their &lt;a href="https://www.mock-server.com/mock_server/getting_started.html"&gt;documentation&lt;/a&gt; and the &lt;a href="https://github.com/mock-server/mockserver/blob/master/mockserver-examples/json_examples.md"&gt;examples&lt;/a&gt;. Even better, try it out yourself!&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;MockServer is a lightweight, convenient solution for mocking APIs. The Docker image is only 276MB! A basic setup only requires Docker and a JSON file, which is powerful enough for more use cases. The hot reloading feature makes it easy to set up and test.&lt;/p&gt;

&lt;p&gt;However, while a JSON file has its advantages, especially when paired with Docker, it can be challenging in terms of readability and comprehension. For complex APIs, opting to define specifications in Java or Javascript and then exporting them to JSON might be a better alternative. To learn more, check out &lt;a href="https://www.mock-server.com/mock_server/creating_expectations.html"&gt;Creating expectations&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This article only covered the basics, but I hope it got you interested in trying out MockServer the next time you need to fake an HTTP(s) endpoint.&lt;/p&gt;

&lt;p&gt;Thanks for reading, and happy coding!&lt;/p&gt;

</description>
      <category>todayilearned</category>
      <category>webdev</category>
      <category>tutorial</category>
      <category>testing</category>
    </item>
    <item>
      <title>Exploring The Magic of Python Through The Awesome Slumber Library</title>
      <dc:creator>Lucy Linder</dc:creator>
      <pubDate>Mon, 08 Apr 2024 12:10:00 +0000</pubDate>
      <link>https://dev.to/derlin/exploring-the-magic-of-python-through-the-awesome-slumber-library-22pj</link>
      <guid>https://dev.to/derlin/exploring-the-magic-of-python-through-the-awesome-slumber-library-22pj</guid>
      <description>&lt;p&gt;Slumber is one of those libraries you don't need, but can't live without once you learn about it (much like &lt;a href="https://www.attrs.org/en/stable/"&gt;attrs&lt;/a&gt;!). It covers a generic use case - interacting with RESTful services - and is a prime example of what only the Python language lets you do. Intrigued? Let me explain!&lt;/p&gt;

&lt;p&gt;🤲 (NOTE) I recently published a new library - &lt;a href="https://github.com/derlin/mantelo"&gt;mantelo&lt;/a&gt; - that leverages the magic of slumber to provide a fully-fledged Keycloak Admin Client. Read more in my other article → &lt;a href="https://blog.derlin.ch/introducing-mantelo"&gt;https://blog.derlin.ch/introducing-mantelo&lt;/a&gt;.&lt;/p&gt;






&lt;ul&gt;
&lt;li&gt;
Introduction to slumber

&lt;ul&gt;
&lt;li&gt;In a few words&lt;/li&gt;
&lt;li&gt;Slumber in action: an example with the dev.to API&lt;/li&gt;
&lt;li&gt;A more formal explanation of the "translation"&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
Delving into the magic

&lt;ul&gt;
&lt;li&gt;Theory first: dunder methods&lt;/li&gt;
&lt;li&gt;A simple implementation (&amp;lt; 20 lines!)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;






&lt;h2&gt;
  
  
  Introduction to slumber
&lt;/h2&gt;

&lt;h3&gt;
  
  
  In a few words
&lt;/h3&gt;

&lt;p&gt;From the documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Slumber is a python library that provides a convenient yet powerful object oriented interface to ReSTful APIs. It acts as a wrapper around the excellent &lt;a href="http://python-requests.org/"&gt;requests library&lt;/a&gt; and abstracts away the handling of urls, serialization, and processing requests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In short, slumber wraps a &lt;code&gt;requests.Session&lt;/code&gt; and allows you to use Pythonic constructs to call RESTful APIs. Put even more simply, it &lt;em&gt;translates Python to HTTP calls&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Still a bit fuzzy, isn't it? Let's look at a concrete example!&lt;/p&gt;

&lt;h3&gt;
  
  
  Slumber in action: an example with the dev.to API
&lt;/h3&gt;

&lt;p&gt;To better understand, let's assume we need to interact with &lt;a href="https://developers.forem.com/api/v1"&gt;dev.to's Forem API v1&lt;/a&gt;. First, let's spin up a REPL, import slumber (after a &lt;code&gt;pip install&lt;/code&gt;) and create an API object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;slumber&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;API&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember slumber doesn't know about the dev.to API. Yet, we can use it to query any of its endpoints in the following manner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Get my profile image
# ⮕ GET https://dev.to/api/profile_images/{username}
&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;profile_images&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;derlin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type_of&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;profile_image&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...}&lt;/span&gt;

&lt;span class="c1"&gt;# Get one of my articles on dev.to
# ⮕ GET https://dev.to/api/articles?username=derlin&amp;amp;per_page=1
&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;derlin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;per_page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type_of&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;article&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1750725&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}]&lt;/span&gt;

&lt;span class="c1"&gt;# Try to create a user (oops, I am not allowed!)
# ⮕ POST https://dev.to/api/admin/users &amp;lt;body&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a@a.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;HttpClientError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;admin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last call raised an exception, &lt;code&gt;401: unauthorized&lt;/code&gt;. Normal, I am not authenticated. To change this, let's recreate the &lt;code&gt;api&lt;/code&gt; object, this time passing some &lt;em&gt;auth&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Since the Forem API uses custom headers for authentication instead of a known authentication mechanism, we can't rely on the &lt;a href="https://requests.readthedocs.io/en/latest/user/authentication/"&gt;built-ins&lt;/a&gt; offered by requests (and thus slumber). So let's create our own authentication class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;requests.auth&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AuthBase&lt;/span&gt;

&lt;span class="c1"&gt;# You can create an API key in dev.to's Settings &amp;gt; Extensions
&lt;/span&gt;&lt;span class="n"&gt;DEVTO_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;xxx&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# A custom Auth class that adds the right header
# to every request
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DevToAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthBase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
      &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;api-key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DEVTO_API_KEY&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="c1"&gt;# Tell slumber to use the custom auth
&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;API&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;DevToAuth&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test it, let's query my articles again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type_of&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;article&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1750725&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Magical, right?&lt;/p&gt;

&lt;p&gt;Under the hood, slumber uses a &lt;code&gt;requests.Session&lt;/code&gt; , which can be tuned in case we need to add headers or other things. Another (simpler) way of authenticating would thus be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;api-key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DEVTO_API_KEY&lt;/span&gt;

&lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;API&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example shows all you need to know about slumber.&lt;/p&gt;

&lt;h3&gt;
  
  
  A more formal explanation of the "translation"
&lt;/h3&gt;

&lt;p&gt;How does this translation from Python to an HTTP call actually works?&lt;/p&gt;

&lt;p&gt;As you may have guessed from the dev.to example, the slumber &lt;code&gt;api&lt;/code&gt; object starts with the base URL. Every property or method is then used to add to this base. When it reaches a method that looks like an HTTP method (&lt;code&gt;.get()&lt;/code&gt;, &lt;code&gt;.post()&lt;/code&gt;, &lt;code&gt;.delete()&lt;/code&gt;, ...), slumber puts together the final URL, makes the call, parses the response, and returns either the response body (as a dictionary) or an exception.&lt;/p&gt;

&lt;p&gt;And since an image is worth a thousand words:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZQ1ykzM0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.hashnode.com/res/hashnode/image/upload/v1711986071189/160dc081-0f06-439f-a752-8ce2d53c9d11.png%2520align%3D" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZQ1ykzM0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.hashnode.com/res/hashnode/image/upload/v1711986071189/160dc081-0f06-439f-a752-8ce2d53c9d11.png%2520align%3D" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Delving into the magic
&lt;/h2&gt;

&lt;p&gt;The "translation" explained above seems rather complex. Yet, the whole slumber library is less than 1000 lines of code! Let's see how the "Python magic" makes it possible by re-implementing the translation ourselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Theory first: dunder methods
&lt;/h3&gt;

&lt;p&gt;In Python, &lt;em&gt;dunder methods&lt;/em&gt;, short for "double underscore" methods, are &lt;em&gt;special methods&lt;/em&gt; (also called &lt;em&gt;magic methods&lt;/em&gt;) that define behavior for built-in Python operations. For example, &lt;code&gt;__init__&lt;/code&gt; initializes newly created objects, &lt;code&gt;__repr__&lt;/code&gt; returns a string representation of an object, and &lt;code&gt;__add__&lt;/code&gt; defines the behavior of the &lt;code&gt;+&lt;/code&gt; operator. The ability to define/override such methods at the class level is one of the distinguishing traits of Python.&lt;/p&gt;

&lt;p&gt;For our purpose, we need to familiarize ourselves with two dunders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;__call__(self)&lt;/code&gt; : this method enables instances to be called as if they were functions / lets you define what happens when using parentheses on class instances (&lt;code&gt;my_instance()&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;__getattr__(self, item)&lt;/code&gt;: this method is invoked when an undefined attribute is accessed on an object. If not defined, the normal behavior is to raise an &lt;code&gt;AttributeError&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  A simple implementation (&amp;lt; 20 lines!)
&lt;/h3&gt;

&lt;p&gt;First, let's create a class that has a base URL and implements two HTTP methods (body left as an exercise 😉):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# GET &amp;lt;url&amp;gt;
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# POST &amp;lt;url&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this base, we now have to implement the URL construction. How? Let's start with the addition of a path to the URL. In slumber, we do this by using an attribute &lt;em&gt;unknown&lt;/em&gt; to the instance. Does it ring a bell?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ... rest of the implementation ...
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__getattr__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, whenever we call an attribute on a &lt;code&gt;Resource&lt;/code&gt;, it returns a new &lt;code&gt;Resource&lt;/code&gt; with the path segment added to the URL. What about path parameters? Well, same principle, just another dunder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ... rest of the implementation ...
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path_param&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path_param&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final touch is to bootstrap the whole thing by making the API object also return a &lt;code&gt;Resource&lt;/code&gt; when an unknown attribute is accessed. A full implementation would thus look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;qs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;qs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query_params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__getattr__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path_param&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path_param&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;API&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__getattr__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's try this out!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;API&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/api/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lala&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;foo_bar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;buzz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;lala&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;foo_bar&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;buzz&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these 20 lines of code, we demystified all of slumber's magic. For this kind of thing, Python is quite awesome 😎.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Even though I am more and more drawn to typed languages, Python has some nice tricks in its sleeves. I love how slumber leveraged it to provide a simple yet useful library. It is a prime example of a &lt;em&gt;good&lt;/em&gt; use of dunder methods.&lt;/p&gt;

&lt;p&gt;I hope you'll remember slumber the next time you need to interact with an API!&lt;/p&gt;




&lt;p&gt;If you liked this article, leave a comment or a thumbs up, or share it around ... This would help keep my motivation up!&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>todayilearned</category>
    </item>
    <item>
      <title>Introducing Mantelo - The Best Keycloak Admin Client for Python</title>
      <dc:creator>Lucy Linder</dc:creator>
      <pubDate>Tue, 02 Apr 2024 12:10:00 +0000</pubDate>
      <link>https://dev.to/derlin/introducing-mantelo-the-best-keycloak-admin-client-for-python-347m</link>
      <guid>https://dev.to/derlin/introducing-mantelo-the-best-keycloak-admin-client-for-python-347m</guid>
      <description>&lt;p&gt;I'm thrilled to present my newest open-source project! &lt;a href="https://mantelo.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;Mantelo&lt;/a&gt; is a super small yet super powerful Python library for interacting with the Keycloak Admin API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/derlin/mantelo" rel="noopener noreferrer"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fderlin%2Fmantelo%2Fblob%2Fmain%2Fdocs%2F_static%2Fimages%2Fmantelo-social-preview.png%3Fraw%3Dtrue%2520align%3D"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Mantelo [manˈtelo], from German "&lt;em&gt;Mantel&lt;/em&gt;", from Late Latin "&lt;em&gt;mantum&lt;/em&gt;" means "&lt;em&gt;cloak&lt;/em&gt;" in Esperanto.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Born from the frustration of encountering incomplete support in existing libraries, this is &lt;strong&gt;the only Python client that guarantees full coverage of all the routes in the Keycloak Admin RESTful API&lt;/strong&gt;. How? By &lt;em&gt;wrapping instead of implementing&lt;/em&gt;. Let me explain 😊.&lt;/p&gt;






&lt;ul&gt;
&lt;li&gt;The motivation behind mantelo&lt;/li&gt;
&lt;li&gt;
Getting started with mantelo

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🏁&lt;/strong&gt; Installing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🔐&lt;/strong&gt; Authenticating&lt;/li&gt;
&lt;li&gt;📞 Making calls&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;The best library out there, really?&lt;/li&gt;

&lt;li&gt;Mantelo needs YOU&lt;/li&gt;

&lt;/ul&gt;



&lt;p&gt;TOC generated with &lt;a href="https://github.com/derlin/bitdowntoc" rel="noopener noreferrer"&gt;bitdowntoc&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The motivation behind mantelo
&lt;/h2&gt;

&lt;p&gt;When I started working with Keycloak in Python, I first opted for the renowned &lt;a href="https://python-keycloak.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;python-keycloak&lt;/a&gt;, considered the most complete library.&lt;/p&gt;

&lt;p&gt;It worked well until I had to query a resource that was not supported. I diligently submitted a &lt;a href="https://github.com/marcospereirampj/python-keycloak/pull/478" rel="noopener noreferrer"&gt;pull request&lt;/a&gt; to add a &lt;code&gt;get_idp&lt;/code&gt; method, which took three months to be merged, and a bit more to be released. This doesn't mean the maintainers are incompetent (they are great!), just that PRs in significant open-source projects take time.&lt;/p&gt;

&lt;p&gt;Everything proceeded smoothly until I encountered another wall: python-keycloak implements none of the endpoints related to TOTP. I had three choices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Submit PR(s) and postpone everything for months (&lt;em&gt;slow&lt;/em&gt;),&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use raw HTTP requests instead of python-keycloak in part of the code (&lt;em&gt;ugly&lt;/em&gt;),&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a library that supports &lt;em&gt;ALL&lt;/em&gt; the endpoints, so I will never be in this position again (&lt;em&gt;fun!&lt;/em&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It took a weekend for the code, and a few more for the publishing, documentation, and all the other ornaments of an open-source project. But there it is!&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started with mantelo
&lt;/h2&gt;

&lt;p&gt;Mantelo is hosted on &lt;a href="https://github.com/derlin/mantelo" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, available on &lt;a href="https://pypi.org/project/mantelo/" rel="noopener noreferrer"&gt;PyPi&lt;/a&gt;, and hosts its documentation at readthedocs: &lt;a href="https://mantelo.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;https://mantelo.readthedocs.io/en/latest/&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;🏁&lt;/strong&gt; Installing
&lt;/h3&gt;

&lt;p&gt;To no one's surprise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;mantelo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;🔐&lt;/strong&gt; Authenticating
&lt;/h3&gt;

&lt;p&gt;The first step is to create a &lt;code&gt;KeycloakAdmin&lt;/code&gt; and specify a way of authenticating. Mantelo supports natively username/password and client credentials (OpenID), taking care of tokens and refreshes behind the scenes.&lt;/p&gt;

&lt;p&gt;More details are available in the docs, but for the demo let's use the default &lt;code&gt;admin&lt;/code&gt; user with the default &lt;code&gt;admin-cli&lt;/code&gt; client and connect to the &lt;code&gt;master&lt;/code&gt; realm running on a local Keycloak instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mantelo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KeycloakAdmin&lt;/span&gt;

&lt;span class="n"&gt;adm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KeycloakAdmin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_username_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;# base Keycloak URL
&lt;/span&gt;    &lt;span class="n"&gt;server_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# realm to interact with (and authenticate to if not specified otherwise)
&lt;/span&gt;    &lt;span class="n"&gt;realm_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;master&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# Client to use for authentication
&lt;/span&gt;    &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin-cli&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# User to use for authentication
&lt;/span&gt;    &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# Password to use for authentication
&lt;/span&gt;    &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CHANGE-ME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ℹ This code doesn't interact with Keycloak: Mantelo is lazy and only fetches/refreshes the token when needed (e.g. on the first request).&lt;/p&gt;

&lt;h3&gt;
  
  
  📞 Making calls
&lt;/h3&gt;

&lt;p&gt;You are now ready to interact with the Admin API. One advantage of mantelo is that it &lt;strong&gt;&lt;em&gt;doesn't provide any specific method, it only offers an object-oriented interface to the RESTful API&lt;/em&gt;&lt;/strong&gt;. It ensues any endpoint present in the Admin API is callable using the &lt;code&gt;adm&lt;/code&gt; object.&lt;/p&gt;

&lt;p&gt;First, let's look at an example. Let's say you need to manage users. Here is a sample of what you can do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Get the list of users
# ⮕ GET /admin/realms/{realm}/users
&lt;/span&gt;&lt;span class="n"&gt;adm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# Search users for "foo bar"
# ⮕ GET /admin/realms/{realm}/users?search=foo+bar
&lt;/span&gt;&lt;span class="n"&gt;adm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;foo bar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Get user count
# ⮕ GET /admin/realms/{realm}/users/count
&lt;/span&gt;&lt;span class="n"&gt;adm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# Get a specific user by ID
# ⮕ GET /admin/realms/{realm}/users/725209cd-9076-417b-a404-149a3fb8e35b
&lt;/span&gt;&lt;span class="n"&gt;adm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;725209cd-9076-417b-a404-149a3fb8e35b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# Create a new user, with credentials
# ⮕ POST /admin/realms/{realm}/users
&lt;/span&gt;&lt;span class="n"&gt;adm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;example&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;firstName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Winston&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lastName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Smith&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orwell@1984.uk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;emailVerified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credentials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;juliaMyLove&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Did you get it? Instead of implementing methods to reach endpoints, mantelo uses simple rules to translate Python code to HTTP calls. More formally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Each &lt;em&gt;property&lt;/em&gt; is a path appended to the &lt;em&gt;base URL&lt;/em&gt;. If the &lt;code&gt;server_url&lt;/code&gt; is &lt;code&gt;https://localhost:8080&lt;/code&gt; and the realm is &lt;code&gt;master&lt;/code&gt;, the &lt;em&gt;base URL&lt;/em&gt; is &lt;code&gt;https://localhost:8080/admin/realms/master&lt;/code&gt;. Underscores are automatically translated to dashes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The same goes for &lt;em&gt;methods&lt;/em&gt;, useful for specifying path parameters: the first argument is appended to the path.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The actual HTTP call is triggered by ending a "chain" with a call to one of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.get(**kwargs)&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;&lt;code&gt;.options(**kwargs)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.head(**kwargs)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.post(data=None, files=None, **kwargs)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.patch(data=None, files=None, **kwargs)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.put(data=None, files=None, **kwargs)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.delete(**kwargs)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All the above methods have &lt;code&gt;kwargs&lt;/code&gt; which are appended as query parameters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;POST/PUT/PATCH support &lt;code&gt;data&lt;/code&gt; (a dictionary converted to JSON) or &lt;code&gt;files&lt;/code&gt; for specifying the body of the request.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every request returns either the content of the response's body (parsed from JSON) or an instance of &lt;code&gt;HttpException&lt;/code&gt; if the response's status code is not in the &lt;code&gt;2XX&lt;/code&gt; range.&lt;/p&gt;

&lt;p&gt;And that's it! This magic is possible thanks to &lt;a href="https://github.com/samgiles/slumber" rel="noopener noreferrer"&gt;slumber&lt;/a&gt;, a library I will feature in a separate article (stay tuned 😉).&lt;/p&gt;

&lt;h2&gt;
  
  
  The best library out there, really?
&lt;/h2&gt;

&lt;p&gt;I stated earlier I believe mantelo is the best Keycloak Admin library. Let me summarize the premises of such a bold claim:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;mantelo's footprint is ridiculous: it is currently less than 1000 lines of code and has only 3 direct dependencies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;mantelo is the only library that can rightfully claim it supports the whole Keycloak Admin interface and is always up-to-date.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;mantelo abstracts away authentication (and refresh tokens), which is always tricky to get right.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;mantelo gives you access to the exact URL that was called (or the &lt;code&gt;requests.Response&lt;/code&gt; in case of an error) so debugging is easy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;mantelo is flexible: you can tweak it easily if you need to.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I am aware &lt;em&gt;wrapping instead of implementing&lt;/em&gt; has a drawback: if Keycloak changes the Admin API, your code has to be updated (no encapsulation). But I believe it is a small price to pay, especially since Keycloak proved to be careful about retro-compatibility. What do you think?&lt;/p&gt;

&lt;h2&gt;
  
  
  Mantelo needs YOU
&lt;/h2&gt;

&lt;p&gt;I've put a lot of work into this library, not just on the code, but everything around it—like documentation, making it easy to test, setting up the GitHub repo, and more.&lt;/p&gt;

&lt;p&gt;But mantelo isn't complete without users and a community. So please, give it a shot, star it, criticize it, open an issue... I'm excited to see where we can take it together!&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>python</category>
      <category>devjournal</category>
      <category>developer</category>
    </item>
    <item>
      <title>Ever wondered how cloud providers (PaaS) integrate with GitHub? I did.</title>
      <dc:creator>Lucy Linder</dc:creator>
      <pubDate>Tue, 06 Feb 2024 13:15:00 +0000</pubDate>
      <link>https://dev.to/derlin/ever-wondered-how-cloud-providers-paas-integrate-with-github-i-did-3gbj</link>
      <guid>https://dev.to/derlin/ever-wondered-how-cloud-providers-paas-integrate-with-github-i-did-3gbj</guid>
      <description>&lt;p&gt;With GitHub standing out as the leading platform for hosting public code, most cloud providers offer dedicated features for deploying code hosted on GitHub. I have always used them without much thought. That is, until recently.&lt;/p&gt;

&lt;p&gt;Dissecting their inner workings, I found various approaches, some better than others. This gave me a deeper understanding of the GitHub tools available, how they work, and, well... I found this fascinating and wanted to share! So here it is.&lt;/p&gt;






&lt;ul&gt;
&lt;li&gt;The contenders&lt;/li&gt;
&lt;li&gt;
Google Cloud Run: all the power of GitHub Apps

&lt;ul&gt;
&lt;li&gt;Overview&lt;/li&gt;
&lt;li&gt;What is a GitHub App?&lt;/li&gt;
&lt;li&gt;The process&lt;/li&gt;
&lt;li&gt;Wait... What?&lt;/li&gt;
&lt;li&gt;Analysis&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
Azure Static Web Apps: reversing the responsibility

&lt;ul&gt;
&lt;li&gt;Overview&lt;/li&gt;
&lt;li&gt;What is a GitHub OAuth App?&lt;/li&gt;
&lt;li&gt;What is a GitHub Actions workflow?&lt;/li&gt;
&lt;li&gt;The process&lt;/li&gt;
&lt;li&gt;More about the Azure GitHub Action&lt;/li&gt;
&lt;li&gt;Analysis&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
Heroku: keep it simple

&lt;ul&gt;
&lt;li&gt;Overview&lt;/li&gt;
&lt;li&gt;The process&lt;/li&gt;
&lt;li&gt;Analysis&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
&lt;/h2&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The contenders
&lt;/h2&gt;

&lt;p&gt;Today, we look at &lt;a href="https://cloud.google.com/run?hl=en"&gt;Google Cloud Run&lt;/a&gt;, &lt;a href="https://azure.microsoft.com/en-us/products/app-service/static"&gt;Azure Static Web Apps&lt;/a&gt;, and &lt;a href="https://www.heroku.com"&gt;Heroku&lt;/a&gt;. They are not the sole cloud providers out there (not even the bests, check out &lt;a href="https://docs.divio.com"&gt;divio&lt;/a&gt; 😉), but all three are relatively famous and have some sort of GitHub integration.&lt;/p&gt;

&lt;p&gt;By GitHub integration, I mean the cloud provider has a dedicated process to link with a repository hosted on GitHub and provides additional services such as automatic deployments on push or preview apps on pull requests.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Cloud Run: all the power of GitHub Apps
&lt;/h2&gt;

&lt;p&gt;From their landing page, Google Cloud Run allows you to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Run frontend and backend services, batch jobs, deploy websites and applications, and queue processing workloads without the need to manage infrastructure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I use Cloud Run to deploy my little &lt;a href="https://github.com/derlin/rickroller"&gt;rickroller&lt;/a&gt; app.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;When setting up a new service in Cloud Run, you can choose to "&lt;em&gt;continuously deploy revisions from a source repository&lt;/em&gt;". This supports BitBucket, Cloud Repositories, and of course, GitHub. When the latter is selected, here is what the wizard looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YGaEUP3z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.hashnode.com/res/hashnode/image/upload/v1705166514465/2f0812e8-9bf0-4773-b26c-31b5a5ea106e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YGaEUP3z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.hashnode.com/res/hashnode/image/upload/v1705166514465/2f0812e8-9bf0-4773-b26c-31b5a5ea106e.png" alt="Google Cloud Run GitHub integration overview" width="800" height="890"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are two steps here, let's break them down.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a GitHub App?
&lt;/h3&gt;

&lt;p&gt;Cloud Run uses a &lt;a href="https://docs.github.com/en/apps/overview"&gt;GitHub App&lt;/a&gt; (called &lt;a href="https://github.com/apps/google-cloud-build"&gt;Google Cloud Build&lt;/a&gt;), which is a compelling concept. In short, GitHub Apps can act on your behalf, act on themselves (for example open issues), or act outside GitHub by reacting to events (via webhooks). Each App must declare the permissions it requires (e.g. only read repository contents), which determines the subset of API endpoints they can access.&lt;/p&gt;

&lt;p&gt;Something fundamental is that GitHub Apps are &lt;strong&gt;&lt;em&gt;installed at the organization level&lt;/em&gt;&lt;/strong&gt;. An App just needs to &lt;em&gt;exist&lt;/em&gt; to impersonate you, but it needs to be &lt;em&gt;installed&lt;/em&gt; to act on its own. When installing the app in an organization, it is up to you to define the list of repositories it can act on (all, or a selected few).&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The process
&lt;/h3&gt;

&lt;p&gt;Back to Cloud Run. In the first step, Google tells you to &lt;em&gt;authenticate&lt;/em&gt;. Here, it is using OAuth2 to &lt;a href="https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-with-a-github-app-on-behalf-of-a-user"&gt;retrieve an access token to act on your behalf&lt;/a&gt;. This token will have the permissions both you AND the app have (impersonation). It is important to note that currently, GitHub App user tokens are valid for 8 hours (with a refresh token valid for 6 months)! So don't forget to revoke access after setup.&lt;/p&gt;

&lt;p&gt;Once authenticated, Google uses the token to ask the GitHub API about the &lt;em&gt;installations&lt;/em&gt; of the GitHub App you have access to, and for each installation which repositories the installation can manage. This list is used to populate the second dropdown field.&lt;/p&gt;

&lt;p&gt;The "&lt;em&gt;manage connected repositories&lt;/em&gt;" redirects you to GitHub, where you can see your organizations and potentially install (or update the installation of) the App. After installing an app on an organization (and giving it access to some repos), Google updates the dropdown list displayed in the wizard.&lt;/p&gt;

&lt;p&gt;Finally, once you select a repository in the list, Google saves somewhere the repository information and the corresponding installation ID of the App. With this, Google can &lt;a href="https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation"&gt;authenticate as a GitHub App installation&lt;/a&gt; and start doing things. In this case, "doing things" is reacting to push events (via a webhook) to pull and redeploy your code on Cloud Run (and many other cool things that I won't list there). Its range of actions is limited by the set of permissions of the App.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Wait... What?
&lt;/h3&gt;

&lt;p&gt;Why all this complexity you ask? Let's break it down one more time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Why the need for a user access token in the first step&lt;/em&gt;? A GitHub App can list all its installations, but cannot determine which of those &lt;em&gt;you&lt;/em&gt; have access to.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Why the need to install the app in the second step&lt;/em&gt; (and not just use the user access token)? The user access token generated by a GitHub App has an expiration date. Google needs to perform actions without you being around to give your blessing. Only &lt;em&gt;installations&lt;/em&gt; can request access tokens to act on themselves.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Analysis
&lt;/h3&gt;

&lt;p&gt;The way Google Cloud Run works may seem overly complicated, but there is currently no easier way to associate a GitHub App with a specific repository on GitHub (except by asking the user to enter manually the name of the repo and the installation ID, which is not user-friendly). Once Google associates a GitHub App Installation ID to the repository, it has all the power a GitHub App entails: it can do &lt;em&gt;anything&lt;/em&gt;, being only limited by its permissions (which can be updated). This is a very stable and future-proof way of integrating with GitHub.&lt;/p&gt;

&lt;p&gt;Another advantage of GitHub Apps is that they act on their own. If they publish a comment on a pull request or do a push, you will see the name of the App as the author. This allows for easy monitoring.&lt;/p&gt;

&lt;p&gt;A user is free to opt out at any point by deleting the GitHub App installation.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Azure Static Web Apps: reversing the responsibility
&lt;/h2&gt;

&lt;p&gt;Azure Static Web Apps offers "&lt;em&gt;static content hosting and dynamic scale for integrated serverless APIs&lt;/em&gt;".&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;Contrary to Google, which implements a "pull" mechanism, Azure went for the "push" approach, taking advantage of GitHub OAuth Apps and GitHub Actions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--yXnxo585--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.hashnode.com/res/hashnode/image/upload/v1706440924513/285543c0-469c-4b43-b081-afd39471c0cd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yXnxo585--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.hashnode.com/res/hashnode/image/upload/v1706440924513/285543c0-469c-4b43-b081-afd39471c0cd.png" alt="Azure Static Web App Github integration process" width="800" height="680"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a GitHub OAuth App?
&lt;/h3&gt;

&lt;p&gt;Unlike GitHub Apps, you do not install OAuth Apps because they can only &lt;em&gt;act on behalf of a user&lt;/em&gt;. They cannot act on themselves as GitHub Apps can. It is not possible to limit the set of repositories an OAuth app can access, only their permissions (e.g. read vs read/write), and their access tokens never expire. There is no reason to use OAuth Apps nowadays, but they were the first historically, thus many integrations still use them.&lt;/p&gt;

&lt;p&gt;To know more, read &lt;a href="https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/differences-between-github-apps-and-oauth-apps"&gt;Differences between GitHub Apps and OAuth apps&lt;/a&gt; (the GitHub documentation is amazing, btw).&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a GitHub Actions workflow?
&lt;/h3&gt;

&lt;p&gt;From the &lt;a href="https://docs.github.com/en/actions/using-workflows/about-workflows"&gt;documentation&lt;/a&gt;,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A workflow is a configurable automated process that will run one or more [GitHub Actions] jobs. Workflows are defined by a YAML file checked in to your repository and will run when triggered by an event in your repository, or they can be triggered manually, or at a defined schedule.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To paraphrase, workflows allow running automated tasks on GitHub (using runners) to implement CI (Continuous Integration, such as running tests) and CD (Continuous Deployment, such as building and publishing a Docker image or deploying a package to NPM).&lt;/p&gt;

&lt;p&gt;Gitlab and others also have CI/CD. What is specific to GitHub is the notion of &lt;a href="https://docs.github.com/en/actions/quickstart"&gt;GitHub Actions&lt;/a&gt;. A workflow is composed of jobs that have a list of steps. A step can be a shell script or a call to an action. Think of an action as a way of packing complex logic into a simple component that can be reused, thus &lt;em&gt;extending&lt;/em&gt; GitHub capabilities. Public Actions are stored on public GitHub repositories and may be published to the &lt;a href="https://github.com/marketplace"&gt;MarketPlace&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The process
&lt;/h3&gt;

&lt;p&gt;So, back to the wizard. First, Azure asks you to authenticate to GitHub (using the OAuth App), so it can act on your behalf. Next, it retrieves the list of your repositories and branches from the GitHub API to populate the dropdowns. Once you choose the target repository, it asks you to select a "build preset" (it currently supports frameworks such as React or Angular, and static website generators such as Gatsby or Hugo). Those presets are used to generate a GitHub Workflow file, which itself uses a GitHub Action maintained by Azure called &lt;a href="https://github.com/Azure/static-web-apps-deploy"&gt;static-web-apps-deploy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When hitting deploy, Azure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;commits the workflow file in the &lt;code&gt;.github/workflows&lt;/code&gt; directory of your branch (with you as the author), and&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;updates your repository settings to add environment secrets (under &lt;em&gt;Settings&lt;/em&gt; &amp;gt; &lt;em&gt;Secrets and Variables&lt;/em&gt; &amp;gt; &lt;em&gt;Actions&lt;/em&gt;), such as the secret token used by the workflow to authenticate to Azure.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that this is a one-time action. Afterward, everything is done from GitHub through the magic of the Azure GitHub Action. In other words, it is the workflow running in a GitHub runner that calls an Azure API endpoint to deploy a new version. The responsibility is reversed.&lt;/p&gt;

&lt;p&gt;Note that you don't need to use the wizard, you can also manually create the workflow file in your repo (and add the necessary secrets) and the result would be the same. I would even encourage you to go the manual way, to avoid granting Azure any permissions.&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  More about the Azure GitHub Action
&lt;/h3&gt;

&lt;p&gt;The default workflow committed by Azure can be seen here (with slight modifications depending on the preset): &lt;a href="https://learn.microsoft.com/en-us/azure/static-web-apps/build-configuration?tabs=github-actions#build-configuration"&gt;https://learn.microsoft.com/en-us/azure/static-web-apps/build-configuration?tabs=github-actions#build-configuration&lt;/a&gt;. Unfortunately, the GitHub Action, &lt;a href="https://github.com/Azure/static-web-apps-deploy"&gt;Azure/static-web-apps-deploy&lt;/a&gt;, hides its source code inside an executable from a Docker image. A shame, because I would love to analyze it!&lt;/p&gt;

&lt;p&gt;With the default workflow, your Azure static web app is redeployed with the latest changes on every push to the main branch. Here is an output of the GitHub Action run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;Zipping&lt;/span&gt; &lt;span class="n"&gt;App&lt;/span&gt; &lt;span class="n"&gt;Artifacts&lt;/span&gt;
&lt;span class="n"&gt;Done&lt;/span&gt; &lt;span class="n"&gt;Zipping&lt;/span&gt; &lt;span class="n"&gt;App&lt;/span&gt; &lt;span class="n"&gt;Artifacts&lt;/span&gt;
&lt;span class="n"&gt;Uploading&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt; &lt;span class="n"&gt;artifacts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Finished&lt;/span&gt; &lt;span class="n"&gt;Upload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Polling&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;InProgress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.0628832&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Succeeded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;15.1286488&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Deployment&lt;/span&gt; &lt;span class="n"&gt;Complete&lt;/span&gt; &lt;span class="p"&gt;:)&lt;/span&gt;
&lt;span class="n"&gt;Visit&lt;/span&gt; &lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;site&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;blue&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;sea&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;084206610&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.&lt;/span&gt;&lt;span class="n"&gt;centralus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;4.&lt;/span&gt;&lt;span class="n"&gt;azurestaticapps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;net&lt;/span&gt;
&lt;span class="n"&gt;Thanks&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;using&lt;/span&gt; &lt;span class="n"&gt;Azure&lt;/span&gt; &lt;span class="n"&gt;Static&lt;/span&gt; &lt;span class="n"&gt;Web&lt;/span&gt; &lt;span class="n"&gt;Apps&lt;/span&gt;&lt;span class="err"&gt;!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whenever a pull request targeting the main branch is opened, the action deploys instead a preview environment and adds a comment to the pull request with the preview URL. Every change on the pull request will update the preview environment. The preview environment is torn down when the pull request is closed (merged or discarded).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--msV8IulI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.hashnode.com/res/hashnode/image/upload/v1705684222828/9a6d1e13-78f7-477f-a968-da63517de183.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--msV8IulI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.hashnode.com/res/hashnode/image/upload/v1705684222828/9a6d1e13-78f7-477f-a968-da63517de183.png" alt="Azure GitHub action PR comment" width="800" height="147"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is possible to configure preview environments for other branches than &lt;code&gt;main&lt;/code&gt; with a few changes to the GitHub Action's parameters.&lt;/p&gt;

&lt;p&gt;And all of this works flawlessly! The only thing I had to change was the default permissions of the workflows (in &lt;em&gt;Settings&lt;/em&gt; &amp;gt; &lt;em&gt;actions&lt;/em&gt; &amp;gt; &lt;em&gt;workflow permissions&lt;/em&gt;) from &lt;code&gt;read&lt;/code&gt; to &lt;code&gt;read/write&lt;/code&gt; to allow the Action to post comments on pull requests (see &lt;a href="https://github.com/Azure/static-web-apps/issues/797"&gt;this issue for details&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Analysis
&lt;/h3&gt;

&lt;p&gt;I find the Azure approach brilliant.&lt;/p&gt;

&lt;p&gt;Thanks to the GitHub Action, Azure itself doesn't need to know anything about git or GitHub (the content to deploy is sent as a zipped archive). There are no permissions involved since the workflow runs inside your repository. Reacting to pull requests is also easier since it is a built-in feature of workflows. To do the same with a GitHub App, you would require additional permissions, plus a server somewhere reacting to webhooks.&lt;/p&gt;

&lt;p&gt;As a bonus, users can freely choose the version of the action they use, and parametrize it to their liking in a Configuration as Code (CAC) manner. There is no magic outside the repo itself. A GitHub Action is thus more transparent, and its power is only limited to the permissions of the &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; configured in the workflow.&lt;/p&gt;

&lt;p&gt;The only drawback is the use of an OAuth App for automatic setup. OAuth App tokens remain active until they're revoked by the user, meaning Azure may still act on your behalf on all your private repositories after the setup unless you manually do something. I believe Azure discards the token after creating the workflow, but still. My two cents: revoke access to the Azure OAuth App after setup!&lt;/p&gt;

&lt;p&gt;In short, the GitHub Action approach is less expensive, more sustainable, and user-friendly. Good job!&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Heroku: keep it simple
&lt;/h2&gt;

&lt;p&gt;Heroku is a container-based cloud Platform as a Service (PaaS). Acquired in 2010 by Salesforce, it hosts your applications in AWS (without you having to know this fact).&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Overview
&lt;/h3&gt;

&lt;p&gt;Heroku takes a simpler approach, combining a GitHub OAuth App and a repository webhook, while still offering advanced features such as preview apps (deployed on pull requests), automatic deployments, and more.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--cfDr26oP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.hashnode.com/res/hashnode/image/upload/v1706440971720/289cb049-4450-4f06-bd44-b1d412afc560.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cfDr26oP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.hashnode.com/res/hashnode/image/upload/v1706440971720/289cb049-4450-4f06-bd44-b1d412afc560.png" alt="Heroku GitHub integration process" width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The process
&lt;/h3&gt;

&lt;p&gt;To enable GitHub integration, Heroku asks you to allow the Heroku Dashboard OAuth application and give it &lt;em&gt;full control&lt;/em&gt; over &lt;em&gt;all&lt;/em&gt; your public and private repositories across &lt;em&gt;all&lt;/em&gt; the organizations you are admin of. Because it uses an OAuth app, there are no fine-grained permissions: it has access to all or nothing.&lt;/p&gt;

&lt;p&gt;Once authorized, you can search (no dropdown 😳) the name of a repository and "enable" it. Upon pressing enable, Heroku adds a webhook to your repository. From now on, every action on your repository will notify &lt;a href="https://kolkrabbi.heroku.com/hooks/github"&gt;https://kolkrabbi.heroku.com/hooks/github&lt;/a&gt;. The webhook is visible under your repository's &lt;em&gt;Settings&lt;/em&gt; &amp;gt; &lt;em&gt;Webhooks&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;With the webhook in place and the ability to act as you on GitHub (remember that OAuth user tokens do not expire), Heroku is now ready. In the Heroku settings, you can now enable automatic deployments, &lt;a href="https://devcenter.heroku.com/articles/github-integration-review-apps"&gt;Review Apps&lt;/a&gt;, and more.&lt;/p&gt;

&lt;p&gt;In other words, when something happens on the repository, Heroku gets notified via the webhook and takes action depending on the Heroku settings. If it needs to interact with GitHub, it impersonates you. This is how you can see messages on pull requests such as "&lt;em&gt;&amp;lt;your username&amp;gt; deployed &amp;lt;your app&amp;gt; X seconds ago&lt;/em&gt;".&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Analysis
&lt;/h3&gt;

&lt;p&gt;This approach is efficient because, with only one action from you (the authorization of the App), Heroku can do &lt;em&gt;everything&lt;/em&gt;. Adding new features is completely transparent because it listens to all events and asks for all the permissions &lt;em&gt;you&lt;/em&gt; have on GitHub.&lt;/p&gt;

&lt;p&gt;The downside? You are asked to trust Heroku completely and have no way of ensuring it doesn't abuse its power. A security breach such as &lt;a href="https://blog.heroku.com/april-2022-incident-review"&gt;the one of April 2022&lt;/a&gt; (attackers accessed OAuth2 tokens from Heroku and used them to steal secrets stored in private GitHub repositories) can yield big consequences with unrestrained OAuth tokens. Remember: "&lt;em&gt;OAuth tokens remain active until they're revoked by the customer"&lt;/em&gt;. It is up to Heroku to ensure it secures and rotates the tokens properly. Hopefully, they have competent engineers :).&lt;/p&gt;

&lt;p&gt;To be fair, Heroku supported GitHub integrations before GitHub Apps appeared, and &lt;a href="https://devcenter.heroku.com/articles/github-integration#why-does-heroku-need-read-and-write-permissions-to-my-github-repos"&gt;is aware of the issue&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;GitHub Apps allow for finer grained control on repositories, but that has not been integrated into the GitHub integration yet. We cannot commit to a timeline but it’s definitely on our radar. Keep an eye on the Heroku Changelog.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Given their current implementation works perfectly, I doubt the migration to GitHub Apps will be prioritized soon (unless another security breach forces their hands).&lt;/p&gt;

&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This article looked at three different ways for cloud platforms to interact with GitHub:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google Cloud Run&lt;/td&gt;
&lt;td&gt;Pull → GitHub App (+app webhook)&lt;/td&gt;
&lt;td&gt;Limited permissions. Acts as you during setup, acts as itself otherwise.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure Static Web Apps&lt;/td&gt;
&lt;td&gt;Push → GitHub Action&lt;/td&gt;
&lt;td&gt;No permissions. Optional GitHub OAuth for setup only (theoretically one shot).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heroku&lt;/td&gt;
&lt;td&gt;Pull → GitHub OAuth App (+ repository webhook)&lt;/td&gt;
&lt;td&gt;Full permissions, always acts as you.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In my opinion, Azure is the clear winner with an original &lt;em&gt;push&lt;/em&gt; approach. You are responsible for adding a workflow in your repository and you control what happens end-to-end. There is no hidden magic, and you are not required to give Azure any access to GitHub whatsoever, although you can choose to do so to benefit from a guided setup.&lt;/p&gt;

&lt;p&gt;Google comes second, with the most correct way of implementing a pull approach as of 2023. GitHub Apps have fine-grained permissions and act as themselves, making their actions more transparent on GitHub. GitHub Apps are a bit more complex to manage though, with this whole authorization versus installation concept, and requires a very good wizard to make it simple for users. Google's wizard is not the best in class in this regard (I am not surprised).&lt;/p&gt;

&lt;p&gt;Heroku's implementation based solely on a GitHub OAuth App is outdated and suffers from all the downsides pushing GitHub to ship GitHub Apps in the first place: lack of fine-grained permissions, Apps acting on behalf of (admin) users, no expiration of tokens, and more. It is however the easiest to put in place and maintain from a vendor perspective, and also the easiest for the user.&lt;/p&gt;

&lt;p&gt;The main takeaways from this (already too long) article are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;implementing a GitHub integration is not straightforward and must be planned carefully.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GitHub is an amazing product with many points of extensions: GitHub Apps, GitHub OAuth Apps, and GitHub Actions are today the most prevalent.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As a GitHub user, you may often be asked to authorize an extension. Learn how to read the prompts properly and always think of the security implications before saying yes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Don't forget to review permissions regularly, and revoke access to Apps you do not use anymore. On GitHub, click on your avatar on the top-right, &lt;em&gt;Settings&lt;/em&gt; &amp;gt; &lt;em&gt;Applications&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I hope you enjoyed this article and learned something, let me know in the comments what you think and if you have a better integration approach undocumented here.&lt;/p&gt;

</description>
      <category>github</category>
      <category>devops</category>
      <category>todayilearned</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Diving Deeper into Python Exceptions</title>
      <dc:creator>Lucy Linder</dc:creator>
      <pubDate>Tue, 09 Jan 2024 14:00:00 +0000</pubDate>
      <link>https://dev.to/derlin/diving-deeper-into-python-exceptions-cf1</link>
      <guid>https://dev.to/derlin/diving-deeper-into-python-exceptions-cf1</guid>
      <description>&lt;p&gt;I have been coding in Python for a long time, yet I am puzzled by how little I knew about &lt;code&gt;Exception&lt;/code&gt;s. This post is about some of my recent findings on this topic.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Content&lt;/strong&gt;&lt;/p&gt;



&lt;ul&gt;
&lt;li&gt;A little story&lt;/li&gt;
&lt;li&gt;Exception chaining (and the magic of &lt;code&gt;__context__&lt;/code&gt; )&lt;/li&gt;
&lt;li&gt;Bare except vs except Exception&lt;/li&gt;
&lt;li&gt;Raising shorthands&lt;/li&gt;
&lt;li&gt;Annotating exceptions (3.11+)&lt;/li&gt;
&lt;li&gt;What about &lt;code&gt;UserWarning&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Bonus&lt;/li&gt;
&lt;/ul&gt;






&lt;h2&gt;
  
  
  A little story
&lt;/h2&gt;

&lt;p&gt;I had an interesting use case at work lately: some external library code was "swallowing" another exception, and I needed to get back the message of the initial one somehow, without touching the library itself.&lt;/p&gt;

&lt;p&gt;The library code looked something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenBackend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Decode a JWT token,
&lt;/span&gt;      &lt;span class="c1"&gt;# It may fail for many reasons, detailed in the
&lt;/span&gt;      &lt;span class="c1"&gt;# TokenError message
&lt;/span&gt;      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
         &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TokenError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Empty token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
         &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TokenError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A JWT must have 3 parts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;...:&lt;/span&gt;
         &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TokenError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;# ...
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
      &lt;span class="c1"&gt;# ...
&lt;/span&gt;      &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
         &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;- calls TokenBackend.decode
&lt;/span&gt;      &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;TokenError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
         &lt;span class="c1"&gt;# Here, the library swallows the exception,
&lt;/span&gt;         &lt;span class="c1"&gt;# we LOSE the detailed error message!
&lt;/span&gt;         &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;DecodeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Token could not be decoded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had no clue how to do this except to override the &lt;code&gt;Token&lt;/code&gt; class in my codebase. When I asked my boss about this, he looked at me and said: "&lt;em&gt;just use&lt;/em&gt; &lt;code&gt;__context__&lt;/code&gt;". Huh? Never heard of it. I started digging, and long story short: he was right. This was the perfect solution.&lt;/p&gt;

&lt;p&gt;Those small discoveries happened a lot lately, and I wanted to share them. If this intrigues you, keep reading!&lt;/p&gt;




&lt;h2&gt;
  
  
  Exception chaining (and the magic of &lt;code&gt;__context__&lt;/code&gt; )
&lt;/h2&gt;

&lt;p&gt;So, what is this &lt;code&gt;__context__&lt;/code&gt;??&lt;/p&gt;

&lt;p&gt;Formalised in &lt;a href="https://peps.python.org/pep-3134/"&gt;PEP 3134&lt;/a&gt; (I love PEPs), exceptions in Python 3 have three &lt;em&gt;dunder&lt;/em&gt; attributes that provide &lt;a href="https://docs.python.org/3/library/exceptions.html#exception-context"&gt;information about the context&lt;/a&gt; in which they were raised: &lt;code&gt;__cause__&lt;/code&gt;, &lt;code&gt;__context__&lt;/code&gt; and &lt;code&gt;__suppress_context__&lt;/code&gt;. To understand, let's first make sense of implicit and explicit exception chaining.&lt;/p&gt;

&lt;p&gt;An &lt;em&gt;exception chain&lt;/em&gt; starts when a new exception is raised during the handling of another, for example from an &lt;code&gt;except&lt;/code&gt; clause:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;foo.bar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oops&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nc"&gt;Traceback &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;stdin&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Errno&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;No&lt;/span&gt; &lt;span class="n"&gt;such&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;foo.bar&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="n"&gt;During&lt;/span&gt; &lt;span class="n"&gt;handling&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;above&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;another&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="n"&gt;occurred&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="nc"&gt;Traceback &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;stdin&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oops&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is called an &lt;em&gt;implicit&lt;/em&gt; chain (hence the "&lt;em&gt;During handling ...&lt;/em&gt;"). To make it explicit and clearly state an exception is the &lt;em&gt;cause&lt;/em&gt; of another, one can use the special &lt;code&gt;raise ... from&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;foo.bar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;OSError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oops&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;

&lt;span class="nc"&gt;Traceback &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;stdin&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;FileNotFoundError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Errno&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;No&lt;/span&gt; &lt;span class="n"&gt;such&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;foo.bar&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;above&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="n"&gt;was&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;direct&lt;/span&gt; &lt;span class="n"&gt;cause&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;following&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="nc"&gt;Traceback &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;stdin&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oops&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we now have another log message: "&lt;em&gt;was the direct cause of&lt;/em&gt;". Of course, chains can be longer than two.&lt;/p&gt;

&lt;p&gt;To go back to the context attributes of an exception:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;__suppress_context__&lt;/code&gt; is false by default.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When raising a new exception while another exception is already being handled (in &lt;code&gt;except&lt;/code&gt;, &lt;code&gt;finally&lt;/code&gt; or &lt;code&gt;with&lt;/code&gt;), the new exception’s &lt;code&gt;__context__&lt;/code&gt; attribute is automatically set to the handled exception.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When using &lt;code&gt;raise ... from&lt;/code&gt; , the supplied exception will additionally be saved in the &lt;code&gt;__cause__&lt;/code&gt; attribute of the raised exception, and &lt;code&gt;__suppress_context__&lt;/code&gt; will be set to true.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The default traceback uses those attributes to display stacktraces in the following way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;if &lt;code&gt;__cause__&lt;/code&gt; is present, always show it&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;if &lt;code&gt;__cause__&lt;/code&gt; is &lt;code&gt;None&lt;/code&gt;, show the &lt;code&gt;__context__&lt;/code&gt; only if &lt;code&gt;__suppress_context__&lt;/code&gt; is false.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To ensure you followed, what does this valid Python code prints?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ZeroDivisionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zero!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It only shows the &lt;code&gt;RuntimeError&lt;/code&gt;, because the &lt;code&gt;from&lt;/code&gt; will set the &lt;code&gt;__cause__&lt;/code&gt; (to &lt;code&gt;None&lt;/code&gt;) and the &lt;code&gt;__suppress_context__&lt;/code&gt; (to true).&lt;/p&gt;

&lt;p&gt;Now, this doesn't completely swallow the original exception. Even when using a &lt;code&gt;from&lt;/code&gt;, the initial &lt;code&gt;ZeroDivisionError&lt;/code&gt; is still in the &lt;code&gt;__context__&lt;/code&gt;, just ignored when printing the stacktrace.&lt;/p&gt;

&lt;p&gt;Back to the problem in the introduction, I simply catch the exception &lt;code&gt;e&lt;/code&gt; raised by the library (in &lt;code&gt;Token.__init__&lt;/code&gt;), and then use &lt;code&gt;e.__context__.args[0]&lt;/code&gt; to get the initial exception message.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bare except vs except Exception
&lt;/h2&gt;

&lt;p&gt;I learned this one from the ruff rule &lt;a href="https://docs.astral.sh/ruff/rules/bare-except/"&gt;bare-except (E722)&lt;/a&gt;. When you don't care about which exception is raised, you may be tempted to use a &lt;em&gt;bare except&lt;/em&gt; (but should NOT):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="nf"&gt;do_something&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;- no exception class is called bare except
&lt;/span&gt;   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oops&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bare &lt;code&gt;except&lt;/code&gt; catches &lt;code&gt;BaseException&lt;/code&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://docs.python.org/3/library/exceptions.html#BaseException"&gt;&lt;code&gt;BaseException&lt;/code&gt;&lt;/a&gt; is the common base class of all exceptions. One of its subclasses, &lt;a href="https://docs.python.org/3/library/exceptions.html#Exception"&gt;&lt;code&gt;Exception&lt;/code&gt;&lt;/a&gt;, is the base class of all the non-fatal exceptions. Exceptions which are not subclasses of &lt;a href="https://docs.python.org/3/library/exceptions.html#Exception"&gt;&lt;code&gt;Exception&lt;/code&gt;&lt;/a&gt; are not typically handled, because they are used to indicate that the program should terminate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This &lt;code&gt;except&lt;/code&gt; thus catches &lt;code&gt;Exception&lt;/code&gt;, but also &lt;code&gt;KeyboardInterrupt&lt;/code&gt;, &lt;code&gt;SystemExit&lt;/code&gt;, and other fatal errors, making it hard to interrupt the program (e.g., with Ctrl-C) and potentially disguising other problems or leaving the program in an unexpected state.&lt;/p&gt;

&lt;p&gt;So instead, always specify an exception type, or simply &lt;code&gt;Exception&lt;/code&gt; if you are in doubt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="nf"&gt;do_something&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# now we are good
&lt;/span&gt;   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oops&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Raising shorthands
&lt;/h2&gt;

&lt;p&gt;When re-raising inside an &lt;code&gt;except&lt;/code&gt; clause, you don't need to pass an argument to &lt;code&gt;raise&lt;/code&gt;, as it re-raises the caught exception by default.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ZeroDivisionError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# no need to add "as e"
&lt;/span&gt;   &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;we got a zero here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly, when raising an exception with no argument, no need for parentheses. If an exception class is passed to &lt;code&gt;raise&lt;/code&gt;, it will be implicitly instantiated by calling its constructor with no arguments.&lt;/p&gt;

&lt;p&gt;Hence the following is perfectly valid and more concise (see ruff rule &lt;a href="https://docs.astral.sh/ruff/rules/unnecessary-paren-on-raise-exception/"&gt;unnecessary-paren-on-raise-exception (RSE102)&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Annotating exceptions (3.11+)
&lt;/h2&gt;

&lt;p&gt;Since Python 3.11, it is possible to attach notes to exceptions, effectively enriching their context. This is a very interesting feature, that could replace re-raising an exception with a different message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;
  &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_note&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This was raised as an example&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_note&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Great article btw!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;raise&lt;/span&gt;

&lt;span class="nc"&gt;Traceback &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;stdin&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nb"&gt;ValueError&lt;/span&gt;
&lt;span class="n"&gt;This&lt;/span&gt; &lt;span class="n"&gt;was&lt;/span&gt; &lt;span class="n"&gt;raised&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt;
&lt;span class="n"&gt;Great&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt; &lt;span class="n"&gt;btw&lt;/span&gt;&lt;span class="err"&gt;!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those notes are saved in the &lt;code&gt;__notes__&lt;/code&gt; attribute (list of strings).&lt;/p&gt;




&lt;h2&gt;
  
  
  What about &lt;code&gt;UserWarning&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;If you look at the Python documentation, you will see some strange built-in exceptions such as &lt;code&gt;UserWarning&lt;/code&gt;, &lt;code&gt;DeprecationWarning&lt;/code&gt;, etc. They all inherit from &lt;code&gt;Warning&lt;/code&gt; (which itself inherits from &lt;code&gt;Exception&lt;/code&gt;) but they &lt;strong&gt;are NOT meant to be raised&lt;/strong&gt;. Instead, they are used as &lt;a href="https://docs.python.org/3/library/warnings.html#warning-categories"&gt;warning categories&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In short, &lt;code&gt;Warning&lt;/code&gt; exceptions are to be used with the &lt;a href="https://docs.python.org/3/library/warnings.html#module-warnings"&gt;&lt;code&gt;warnings&lt;/code&gt;&lt;/a&gt; module. There is much more to it, but let's look at a simple example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;warnings&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Explicit category
&lt;/span&gt;    &lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Don&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t use me anymore!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;DeprecationWarning&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Implicit category
&lt;/span&gt;    &lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;- default to UserWarning
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What is nice about &lt;code&gt;warning&lt;/code&gt; is that users have complete control over what is reported, thanks to the warning filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# &amp;lt;stdin&amp;gt;:3: DeprecationWarning: Don't use me anymore!
# &amp;lt;stdin&amp;gt;:5: UserWarning: bar
&lt;/span&gt;&lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# second time, no more warnings printed
&lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;simplefilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ignore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# -&amp;gt; nothing printed
&lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;
&lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;simplefilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# -&amp;gt; raises!
# Traceback (most recent call last):
#   File "&amp;lt;stdin&amp;gt;", line 1, in &amp;lt;module&amp;gt;
#   File "&amp;lt;stdin&amp;gt;", line 3, in foo
# DeprecationWarning: Don't use me anymore!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For all available filters, see &lt;a href="https://docs.python.org/3/library/warnings.html#the-warnings-filter"&gt;The Warnings Filter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So, why use exceptions for that? You guessed it, it simplifies turning warnings into exceptions (&lt;code&gt;error&lt;/code&gt; filter): one just has to raise it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus
&lt;/h2&gt;

&lt;p&gt;This article is already way too long, so here is a bullet list of other interesting subjects and picks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Python 3.11 introduced &lt;code&gt;ExceptionGroup&lt;/code&gt;, a nice way to pack multiple exceptions into one. The new syntax &lt;code&gt;except*&lt;/code&gt; allows filtering groups efficiently. Find out more &lt;a href="https://docs.python.org/3/library/exceptions.html#exception-groups"&gt;in the documentation&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Python supports try-except-&lt;strong&gt;else&lt;/strong&gt;-finally, although I never found a good use case for the &lt;code&gt;else&lt;/code&gt; (also supported in &lt;code&gt;for&lt;/code&gt; loops). The &lt;code&gt;else&lt;/code&gt; block is executed after the &lt;code&gt;try&lt;/code&gt; block, but before the &lt;code&gt;finally&lt;/code&gt; block (if the &lt;code&gt;except&lt;/code&gt; block doesn't run). Exceptions raised inside the &lt;code&gt;else&lt;/code&gt; block are &lt;em&gt;not&lt;/em&gt; caught by the &lt;code&gt;except&lt;/code&gt;. See &lt;a href="https://docs.python.org/3/tutorial/errors.html#handling-exceptions"&gt;Handling exceptions&lt;/a&gt; for more info.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;NotImplementedError&lt;/code&gt; (not to confuse with the constant &lt;code&gt;NotImplemented&lt;/code&gt;) signals a missing implementation &lt;em&gt;that should come one day&lt;/em&gt;. If the feature will never be implemented, raise a &lt;code&gt;TypeError&lt;/code&gt; instead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Exceptions store their &lt;code&gt;__init__&lt;/code&gt; arguments in the &lt;code&gt;args&lt;/code&gt; attribute.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I am always tempted to name my custom exception classes with the &lt;code&gt;Exception&lt;/code&gt; suffix (a remnant of Java perhaps?). However, &lt;a href="https://peps.python.org/pep-0008/#exception-names"&gt;PEP 8&lt;/a&gt; clearly states we should use the suffix &lt;code&gt;Error&lt;/code&gt; for exception class names.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It is possible to catch multiple exceptions using parentheses: &lt;code&gt;except (FooException, BarException)&lt;/code&gt; &lt;br&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Never &lt;code&gt;return&lt;/code&gt; in a &lt;code&gt;finally&lt;/code&gt;: this will override whatever &lt;code&gt;return&lt;/code&gt; you may have inside the &lt;code&gt;try&lt;/code&gt; or the &lt;code&gt;except&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I haven't written in a while, so I hope you enjoyed this article.&lt;/p&gt;

&lt;p&gt;- With ❤️, &lt;a href="https://derlin.ch"&gt;@derlin&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>learning</category>
      <category>development</category>
      <category>coding</category>
    </item>
    <item>
      <title>Python, type hints, and future annotations</title>
      <dc:creator>Lucy Linder</dc:creator>
      <pubDate>Wed, 13 Sep 2023 12:10:00 +0000</pubDate>
      <link>https://dev.to/derlin/python-type-hints-and-future-annotations-4kc5</link>
      <guid>https://dev.to/derlin/python-type-hints-and-future-annotations-4kc5</guid>
      <description>&lt;p&gt;Not long ago, I came across a bug in one of my projects that highlighted very interesting changes in how Python handles type hints at runtime.&lt;/p&gt;

&lt;p&gt;This made me dig into the future of type annotations, and what it means for libraries manipulating type hints at runtime. Spoiler alert: It has to do with &lt;a href="https://peps.python.org/pep-0563/"&gt;PEP 563&lt;/a&gt;.&lt;/p&gt;






&lt;ul&gt;
&lt;li&gt;The bug&lt;/li&gt;
&lt;li&gt;
A brief history of type hints

&lt;ul&gt;
&lt;li&gt;Basics of type hints&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Introducing PEP 563, or what lies behind future annotations&lt;/li&gt;
&lt;li&gt;The explanation&lt;/li&gt;
&lt;li&gt;
How to get the return type of a function after PEP 563?

&lt;ul&gt;
&lt;li&gt;In Python 3.10 and newer&lt;/li&gt;
&lt;li&gt;In Python 3.9 and older&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;A word of caution&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;TOC generated with &lt;a href="https://github.com/derlin/bitdowntoc"&gt;bitdowntoc&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;I have a Django API (&lt;a href="https://www.django-rest-framework.org/"&gt;django-rest-framework&lt;/a&gt; - drf) which uses &lt;a href="https://github.com/axnsan12/drf-yasg"&gt;drf-yasg&lt;/a&gt; to generate an OpenAPI schema at runtime. You don't need to know the intricacies of those libraries, just that API endpoints return serializers, and that drf-yasg inspects them at runtime to generate a schema with the proper fields and types.&lt;/p&gt;

&lt;p&gt;Now, the problem arose with a snippet similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;rest_framework&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;serializers&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MySerializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serializers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Serializer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="c1"&gt;# This allows to return something that is not a direct
&lt;/span&gt;   &lt;span class="c1"&gt;# attribute of a model, but a "computed" value (function)
&lt;/span&gt;   &lt;span class="n"&gt;is_foo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serializers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SerializerMethodField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

   &lt;span class="c1"&gt;# The type hint (-&amp;gt; bool) is used by drf-yasg to
&lt;/span&gt;   &lt;span class="c1"&gt;# guess the proper type is "is_foo"
&lt;/span&gt;   &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_is_foo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_foo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, drf-yasg determines the type of &lt;code&gt;is_foo&lt;/code&gt; based on the type hints of &lt;code&gt;get_is_foo&lt;/code&gt;. The annotation says &lt;code&gt;bool&lt;/code&gt;, so this converts correctly to the OpenAPI schema &lt;code&gt;{"is_foo": "BOOLEAN"}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, upon refactoring, I added the following at the top of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suddenly, the schema became &lt;code&gt;{"is_foo": "STRING"}&lt;/code&gt;. What?? (Note that it took me a while to pinpoint that the problem came from this import, but let's keep the story short).&lt;/p&gt;

&lt;p&gt;If you understand why, you can stop here. If, like me, you find this utterly confusing and want to understand what is going on, keep reading!&lt;/p&gt;




&lt;h2&gt;
  
  
  A brief history of type hints
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;(I digress briefly, so feel free to skip this section).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In 2006, Python 3 introduced a syntax for adding &lt;em&gt;arbitrary&lt;/em&gt; metadata annotations to Python functions (&lt;a href="https://peps.python.org/pep-3107"&gt;PEP 3107: Function Annotations&lt;/a&gt;). (Ab)used by many 3rd parties in different ways, Python 3.5 finally formalized the semantics in &lt;a href="https://peps.python.org/pep-0484/"&gt;PEP 484: Type Hints&lt;/a&gt; (Python 3.5, October 2014 - see also &lt;a href="https://peps.python.org/pep-0483/"&gt;PEP 483 – The Theory of Type Hints&lt;/a&gt;) and added the &lt;code&gt;typing&lt;/code&gt; module to the standard library.&lt;/p&gt;

&lt;p&gt;Over the years, the type hinting system evolved, offering new syntaxes and meta-classes. This is not only drastically changing the way we write Python code, but also the way we ship it to the masses.&lt;/p&gt;

&lt;p&gt;Since Python 3.7, packages can incorporate type information and stubs files, making the life of IDEs and type checkers such as MyPy, pytype, etc. easier (It does, however, make the life of library maintainer a bit more complicated). If you want to know more, read &lt;a href="https://peps.python.org/pep-0561/"&gt;PEP 561: Distributing and Packaging Type Information&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is to say, type hints are now really taking off. I would not be surprised to find type hints ubiquitous in a few years. Javascript is taking the same route, deviating from the purely scripted language to something more structured.&lt;/p&gt;




&lt;h3&gt;
  
  
  Basics of type hints
&lt;/h3&gt;

&lt;p&gt;I wasn't aware of most of the intricacies of type hints until I tried &lt;a href="https://blog.derlin.ch/my-unsuccessful-journey-of-migrating-a-large-django-project-to-mypy"&gt;migrating a large project to mypy&lt;/a&gt; (a good read if you are in a similar process), but I assume most of you are now familiar with the basics of type hints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
  &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;foo&lt;/code&gt; can now be rewritten without imports, thanks to &lt;a href="https://peps.python.org/pep-0585/"&gt;PEP 585 – Type Hinting Generics In Standard Collections&lt;/a&gt; (Python 3.9, March 2019) and &lt;a href="https://peps.python.org/pep-0604/"&gt;PEP 604: Allow writing union types as X | Y&lt;/a&gt; (Python 3.10, May 2020):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Introducing PEP 563, or what lies behind future annotations
&lt;/h2&gt;

&lt;p&gt;If you ever tried using new type hinting features in old Python versions or struggled with forward references, you may have stumbled upon (and used) the famous &lt;em&gt;future import&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding this line at the top of the file makes the snippet "work" (that is, not throw exceptions) from Python 3.7. Ever wondered why?&lt;/p&gt;

&lt;p&gt;Well, the only thing this import does is turn on &lt;a href="https://peps.python.org/pep-0563/"&gt;PEP 563: Postponed Evaluation of Type Annotations&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This PEP proposes changing function annotations and variable annotations so that they are &lt;strong&gt;no longer evaluated at function definition time&lt;/strong&gt;. Instead, they are preserved in &lt;code&gt;__annotations__&lt;/code&gt; in &lt;strong&gt;string form&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Treating all annotations as string &lt;strong&gt;&lt;em&gt;at runtime&lt;/em&gt;&lt;/strong&gt; have clear advantages, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;It removes issues with forward references&lt;/em&gt;. Annotations can now refer to a name that has not yet been defined.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;It gives a simple way to deal with cyclic references&lt;/em&gt;. We can import the names when running type checks (&lt;code&gt;if typing.TYPE_CHECKING: &amp;lt;imports&amp;gt;&lt;/code&gt;) and leave them out at runtime.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;It improves runtime performances&lt;/em&gt;. Type hints are no longer executed at module import time, which is computationally expensive.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This little trick, turning type hints into strings, also explains why we can use new type hint features in old Python versions. Even if &lt;code&gt;str | None&lt;/code&gt; is invalid in Python 3.9 (&lt;code&gt;TypeError: unsupported operand type(s) for |: 'str' and 'NoneType'&lt;/code&gt;), the string &lt;code&gt;'str | None'&lt;/code&gt; is perfectly fine!&lt;/p&gt;

&lt;p&gt;All those advantages explain why PEP 563 is planned to become the default behaviour soon:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;from __future__ import annotations&lt;/code&gt; was previously scheduled to become mandatory in Python 3.10, but the Python Steering Council twice decided to delay the change (&lt;a href="https://mail.python.org/archives/list/python-dev@python.org/message/CLVXXPQ2T2LQ5MP2Y53VVQFCXYWQJHKZ/"&gt;announcement for Python 3.10&lt;/a&gt;; &lt;a href="https://mail.python.org/archives/list/python-dev@python.org/message/VIZEBX5EYMSYIJNDBF6DMUMZOCWHARSO/"&gt;announcement for Python 3.11&lt;/a&gt;). No final decision has been made yet.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The explanation
&lt;/h2&gt;

&lt;p&gt;So, back to our bug.&lt;/p&gt;

&lt;p&gt;For a long time, the &lt;code&gt;inspect&lt;/code&gt; module was the best way to determine a method's return type using annotations. It is what the drf-yasg library uses in &lt;a href="https://github.com/axnsan12/drf-yasg/blob/1.21.7/src/drf_yasg/inspectors/field.py#L616"&gt;https://github.com/axnsan12/drf-yasg/blob/1.21.7/src/drf_yasg/inspectors/field.py#L616&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="n"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_annotation&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked perfectly until &lt;a href="https://peps.python.org/pep-0563/"&gt;PEP 563: Postponed Evaluation of Type Annotations&lt;/a&gt; came along. When enabled, all annotations are suddenly &lt;code&gt;str&lt;/code&gt;, so that was previously the &lt;em&gt;type&lt;/em&gt; &lt;code&gt;bool&lt;/code&gt; becomes the &lt;em&gt;string&lt;/em&gt; &lt;code&gt;'bool'&lt;/code&gt;!&lt;/p&gt;




&lt;h2&gt;
  
  
  How to get the return type of a function after PEP 563?
&lt;/h2&gt;

&lt;p&gt;For now, library maintainers need to assume annotations may or may &lt;em&gt;not&lt;/em&gt; be stringized, and support both. This has been made easier for Python 3.10, but the transition will take time. Let's review our options.&lt;/p&gt;

&lt;h3&gt;
  
  
  In Python 3.10 and newer
&lt;/h3&gt;

&lt;p&gt;From the documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Python 3.10 adds a new function to the standard library: &lt;a href="https://docs.python.org/3/library/inspect.html#inspect.get_annotations"&gt;&lt;code&gt;inspect.get_annotations()&lt;/code&gt;&lt;/a&gt;. In Python versions 3.10 and newer, calling this function &lt;strong&gt;is the best practice&lt;/strong&gt; for accessing the annotations dict of any object that supports annotations. This function &lt;strong&gt;can&lt;/strong&gt; also “un-stringize” stringized annotations for you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To turn on the "un-stringize" feature, simply pass the parameter &lt;code&gt;eval_str=True&lt;/code&gt; :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;__future__&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;
&lt;span class="c1"&gt;# eval_str does the magic
&lt;/span&gt;&lt;span class="n"&gt;ann&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_annotations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eval_str&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;ann&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, this new function looks up the &lt;code&gt;foo.__annotations__&lt;/code&gt; dictionary and calls &lt;code&gt;eval&lt;/code&gt; on values of type &lt;code&gt;str&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you never heard of &lt;code&gt;__annotations__&lt;/code&gt;, it is a mutable dictionary &lt;em&gt;"mapping parameter names to the evaluated annotation expression&lt;/em&gt;". It was introduced in Python 3.0 in 2006 (see &lt;a href="https://peps.python.org/pep-3107/"&gt;PEP 3107 – Function Annotations&lt;/a&gt;). As its name suggests, &lt;code&gt;return&lt;/code&gt; is a special entry key reserved for return annotations.&lt;/p&gt;

&lt;p&gt;Note that &lt;code&gt;inspect.signature&lt;/code&gt; is also updated to support &lt;code&gt;eval_str&lt;/code&gt;, but it just defers the work to &lt;code&gt;get_annotations()&lt;/code&gt;, so better to call the latter directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  In Python 3.9 and older
&lt;/h3&gt;

&lt;p&gt;In older versions, getting the actual type is tricky, even before PEP 563.&lt;/p&gt;

&lt;p&gt;First, as explained in &lt;a href="https://docs.python.org/3/howto/annotations.html#accessing-the-annotations-dict-of-an-object-in-python-3-9-and-older"&gt;Accessing The Annotations Dict Of An Object In Python 3.9 And Older&lt;/a&gt;, just getting the annotation has some flaws. Without going into the details, the safest is to use this code snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_return_annotation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ann&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__dict__&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__annotations__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ann&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__annotations__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ann&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{})[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can return a &lt;code&gt;type&lt;/code&gt;, a &lt;code&gt;str&lt;/code&gt;, or &lt;code&gt;None&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the case of a &lt;code&gt;str&lt;/code&gt;, we may need to "un-stringize" it. Again, the doc gives some pointers in &lt;a href="https://docs.python.org/3/howto/annotations.html#manually-un-stringizing-stringized-annotations"&gt;Manually Un-Stringizing Stringized Annotations&lt;/a&gt;, but the gist is to use &lt;code&gt;eval&lt;/code&gt; and see if this works. But there are many catches... So what do we do?&lt;/p&gt;

&lt;p&gt;From my own experience (and deviating from the docs), I suggest one of two approaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Either give a shot at &lt;a href="https://github.com/shawwn/get-annotations"&gt;get-annotations&lt;/a&gt;. This is a very small library that backports &lt;a href="https://docs.python.org/3/library/inspect.html#inspect.get_annotations"&gt;&lt;code&gt;inspect.get_annotations()&lt;/code&gt;&lt;/a&gt; (with &lt;code&gt;eval_str&lt;/code&gt; support 🤗) to older Python versions. It doesn't have many stars on GitHub but seems to deserve more.&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Or try &lt;code&gt;typing.get_type_hints&lt;/code&gt;. Python 3.7's doc says "&lt;em&gt;In addition, forward references encoded as string literals are handled by evaluating them in&lt;/em&gt; &lt;code&gt;globals&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;locals&lt;/code&gt; &lt;em&gt;namespaces&lt;/em&gt;", which roughly translates to "&lt;em&gt;if&lt;/em&gt; &lt;code&gt;str&lt;/code&gt;&lt;em&gt;, I will try my best to still return a&lt;/em&gt; &lt;code&gt;type&lt;/code&gt;".&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  A word of caution
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;future.annotations&lt;/code&gt; lets you use anything as annotations, including features that are not yet available (or even "garbage"). This is very dangerous if you plan on reading types at runtime.&lt;/p&gt;

&lt;p&gt;For example, if you are using &lt;code&gt;list[str]&lt;/code&gt; or &lt;code&gt;str | None&lt;/code&gt; in a version that does not yet incorporate them, any attempt to &lt;em&gt;unstringize&lt;/em&gt; the annotation will raise an exception (e.g. &lt;code&gt;unsupported operands type(s) for |&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Thus, the safest (especially for libraries) is to &lt;strong&gt;stick with the features present in the lowest version of Python you support&lt;/strong&gt;. In the example above, this means using &lt;code&gt;typing.List&lt;/code&gt; and &lt;code&gt;typing.Union&lt;/code&gt; instead before Python 3.9.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The advent of &lt;a href="https://peps.python.org/pep-0563/"&gt;PEP 563: Postponed Evaluation of Type Annotations&lt;/a&gt; will greatly change the way Python code can access types as runtime, as it turns all annotations from &lt;code&gt;type&lt;/code&gt; to &lt;code&gt;str&lt;/code&gt; representing types.&lt;/p&gt;

&lt;p&gt;Before this PEP becomes the default (Python 3.12?), it can already be activated by importing &lt;code&gt;__future__.annotations&lt;/code&gt;. Any annotation in the same file automatically becomes &lt;em&gt;stringized&lt;/em&gt; before being stored in the &lt;code&gt;__annotations__&lt;/code&gt; dict.&lt;/p&gt;

&lt;p&gt;If a piece of code needs to get the return type of a function (or any other annotation) at runtime, it needs to take this new reality into account.&lt;/p&gt;

&lt;p&gt;In summary, here are the different ways of properly getting types at runtime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;inspect.get_annotations(f)&lt;/code&gt; → only available for Python 3.10+, it is the new best practice. It deals properly with &lt;em&gt;stringized&lt;/em&gt; annotations when &lt;code&gt;eval_str=True&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;inspect.signature(f)&lt;/code&gt; → properly get the types, but only supports &lt;code&gt;eval_str&lt;/code&gt; since Python 3.10.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;f.__annotations__&lt;/code&gt; → dangerous before Python 3.10, due to some quirks. Never access it directly unless you know what you are doing!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;get_annotations.get_annotations(f)&lt;/code&gt; → very small library that offers &lt;code&gt;eval_str&lt;/code&gt; starting from Python 3.6.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;typing.get_type_hints(f)&lt;/code&gt; → states in the docs that it will do its best to deal with stringized annotations, but test thoroughly before use.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep in mind that stringized annotations let you write anything, but as soon as you try to unstringize them, exceptions will be thrown. So no &lt;code&gt;str | None&lt;/code&gt; before python 3.10!&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>todayilearned</category>
    </item>
  </channel>
</rss>
