<?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: Sam</title>
    <description>The latest articles on DEV Community by Sam (@spwoodcock).</description>
    <link>https://dev.to/spwoodcock</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%2F1620343%2F3753034e-8603-4f8d-8c8c-6a758e5f3ecf.jpg</url>
      <title>DEV Community: Sam</title>
      <link>https://dev.to/spwoodcock</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/spwoodcock"/>
    <language>en</language>
    <item>
      <title>Need webhooks? Just use the Postgres HTTP extension</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Thu, 18 Dec 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/spwoodcock/need-webhooks-just-use-the-postgres-http-extension-2d8k</link>
      <guid>https://dev.to/spwoodcock/need-webhooks-just-use-the-postgres-http-extension-2d8k</guid>
      <description>&lt;h2&gt;
  
  
  Reducing complexity
&lt;/h2&gt;

&lt;p&gt;I previously wrote a blog about&lt;a href="https://spwoodcock.dev/blog/2025-02-golang-webhook-odk/" rel="noopener noreferrer"&gt;using pg_notify and a Golang listener&lt;/a&gt;to create a webhook based on Postgres events.&lt;/p&gt;

&lt;p&gt;I no longer believe this is the best approach, after trying out the&lt;a href="https://github.com/pramsey/pgsql-http" rel="noopener noreferrer"&gt;pgsql-http extension&lt;/a&gt;for this use case.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;http&lt;/code&gt; extension is super simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ensure the extension is installed and enabled.&lt;/li&gt;
&lt;li&gt;Create an SQL trigger for an event in your database.&lt;/li&gt;
&lt;li&gt;Call a function from your trigger that does a http call (GET, POST, etc possible).&lt;/li&gt;
&lt;li&gt;You can send a payload to an external webhook, including DB data, and even an &lt;code&gt;X-API-Key&lt;/code&gt; header for authentication (or whatever you use in your API).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now we no longer need an external service / listener running to wait for the events. Everything can be handled entirely in Postgres (a pattern that I am really loving recently, from cron jobs, webhooks, ML embeddings, etc).&lt;/p&gt;

&lt;h2&gt;
  
  
  How this works in practice
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Adding the extension
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A custom dockerfile is very simple:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM postgres:18 AS pg-18

RUN apt-get update \
    &amp;amp;&amp;amp; apt-get install -y postgresql-18-http \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;br&gt;
    &lt;span&gt;&lt;/span&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You can use the same logic to add the extension to a Debian server install.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I actually build and publish pre-built images based on this exact logic - simply adding the extension into the official image&lt;a href="https://github.com/hotosm/central-webhook/pkgs/container/postgres" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example: &lt;code&gt;docker pull ghcr.io/hotosm/postgres:17-http&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a database trigger
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;If you manage you own database schema, you can add functions and triggers to your hearts content. Just add a &lt;code&gt;http_request&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PERFORM http((
    'POST',
    'https://some.url.com/endpoint',
    http_headers('Content-Type', 'application/json'),
    'application/json',
    webhook_payload::text
)::http_request);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;br&gt;
    &lt;span&gt;&lt;/span&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I was adding this to an existing database schema for&lt;a href="https://github.com/getodk/central" rel="noopener noreferrer"&gt;ODK Central&lt;/a&gt;, so instead opted to build a small Golang CLI for installing and uninstalling the trigger, based on certain events.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the example implementation, see&lt;a href="https://github.com/hotosm/central-webhook" rel="noopener noreferrer"&gt;this repo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>postgres</category>
    </item>
    <item>
      <title>How to add translations to the MapLibre UI?</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Wed, 12 Nov 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/spwoodcock/how-to-add-translations-to-the-maplibre-ui-35na</link>
      <guid>https://dev.to/spwoodcock/how-to-add-translations-to-the-maplibre-ui-35na</guid>
      <description>&lt;h2&gt;
  
  
  How do translations work in MapLibre
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;MapLibre has very little built-in &lt;em&gt;UI&lt;/em&gt; to speak of, as it’s mostly for displaying map data in whichever language the user decides.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;However, there are a small number of control prompts, help texts, and tooltips to consider:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&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%2Fx0l67bxp0bsnoa049ygb.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%2Fx0l67bxp0bsnoa049ygb.png" alt="maplibre-tooltip" width="330" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;As of 2025-11, this consists of:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;defaultLocale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AttributionControl.ToggleAttribution&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Toggle attribution&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AttributionControl.MapFeedback&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Map feedback&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FullscreenControl.Enter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enter fullscreen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FullscreenControl.Exit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Exit fullscreen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GeolocateControl.FindMyLocation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Find my location&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GeolocateControl.LocationNotAvailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Location not available&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LogoControl.Title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MapLibre logo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Map.Title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Map&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Marker.Title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Map marker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NavigationControl.ResetBearing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Reset bearing to north&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NavigationControl.ZoomIn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Zoom in&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NavigationControl.ZoomOut&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Zoom out&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Popup.Close&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Close popup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ScaleControl.Feet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ft&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ScaleControl.Meters&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ScaleControl.Kilometers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;km&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ScaleControl.Miles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ScaleControl.NauticalMiles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GlobeControl.Enable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enable globe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GlobeControl.Disable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Disable globe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TerrainControl.Enable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enable terrain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TerrainControl.Disable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Disable terrain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CooperativeGesturesHandler.WindowsHelpText&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Use Ctrl + scroll to zoom the map&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CooperativeGesturesHandler.MacHelpText&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Use ⌘ + scroll to zoom the map&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CooperativeGesturesHandler.MobileHelpText&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Use two fingers to move the map&lt;/span&gt;&lt;span class="dl"&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;ul&gt;
&lt;li&gt;Currently, the user is required to create their own translation file, then import and use it during MapLibre object instantiation:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;maplibregl&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;maplibre-gl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// fr.ts contains the same content as above, translated&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fr&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./locales/fr.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;maplibregl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;map&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://demotiles.maplibre.org/globe.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Use the translation here&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Introducing maplibre-ui-translations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;In a web app I am developing, it is required for the user to change language dynamically, via a dropdown selector.&lt;/li&gt;
&lt;li&gt;In order to simplify this, I decided to make a community plugin, &lt;a href="https://github.com/spwoodcock/maplibre-ui-translations" rel="noopener noreferrer"&gt;maplibre-ui-translations&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The plugin is simple and has a single purpose: provide community-driven translations for the MapLibre UI elements in various languages, allowing the user to either:
a. Change to a single different language.
b. Support changing language via a dropdown selector or similar.&lt;/li&gt;
&lt;li&gt;It handles the re-rendering of the MapLibre UI on language change via the helper function &lt;code&gt;updateMaplibreLocale&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;maplibregl&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;maplibre-gl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defaultLocale&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;maplibre-gl/src/ui/default_locale&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;updateMaplibreLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;maplibreLocales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;maplibre-ui-translations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;maplibregl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;map&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://demotiles.maplibre.org/globe.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;defaultLocale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#lang-switcher&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectedCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLSelectElement&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;updateMaplibreLocale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedCode&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;h2&gt;
  
  
  Your help is needed!
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If you are bilingual and could assist with the community translations, I have made a &lt;a href="https://hosted.weblate.org/projects/maplibre-ui-translations/maplibre-ui-translations" rel="noopener noreferrer"&gt;Weblate project&lt;/a&gt; for it.&lt;/li&gt;
&lt;li&gt;The current translations are mostly machine translations, so validation of those would also be appreciated!&lt;/li&gt;
&lt;li&gt;The plugin can be found in the &lt;a href="https://maplibre.org/maplibre-gl-js/docs/plugins" rel="noopener noreferrer"&gt;MapLibre Plugin docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>geospatial</category>
      <category>programming</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Hacking the Potensic Atom drone to fly pre-generated waypoint missions</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Sun, 11 May 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/spwoodcock/hacking-the-potensic-atom-drone-to-fly-pre-generated-waypoint-missions-34n6</link>
      <guid>https://dev.to/spwoodcock/hacking-the-potensic-atom-drone-to-fly-pre-generated-waypoint-missions-34n6</guid>
      <description>&lt;h2&gt;
  
  
  1. Overview: Drone Tasking Manager (DroneTM)
&lt;/h2&gt;

&lt;p&gt;About a year ago, a joint initiative between the&lt;a href="https://www.hotosm.org" rel="noopener noreferrer"&gt;Humanitarian OpenStreetMap Team&lt;/a&gt; (HOT) and &lt;a href="https://naxa.com.np" rel="noopener noreferrer"&gt;NAXA&lt;/a&gt; began the development of the Drone Tasking Manager (DroneTM).&lt;/p&gt;

&lt;h3&gt;
  
  
  What is DroneTM?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.hotosm.org/tech-suite/drone-tasking-manager" rel="noopener noreferrer"&gt;DroneTM&lt;/a&gt; is a tool to collaboratively generate base imagery as an alternative to traditional satellite sources. It enables subdivision of an area into ‘tasks’ and then generation of flight plans for teams of local drone operators to fly simultaneously.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.linkedin.com/in/nirajadkry" rel="noopener noreferrer"&gt;Niraj&lt;/a&gt;, the lead dev for DroneTM at NAXA already wrote a&lt;a href="https://www.hotosm.org/updates/building-dronetm" rel="noopener noreferrer"&gt;good blog&lt;/a&gt; explaining the idea, so there is no need for me to reiterate here.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Extra Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Initial research for the tool &lt;a href="https://github.com/hotosm/datm-research" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code repo &lt;a href="https://github.com/hotosm/drone-tm" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hardware Support
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The project initially focused on the DJI Mini 4 Pro—a highly capable, sub-250g drone that supports precise waypoint missions (~$1000).&lt;/li&gt;
&lt;li&gt;Long-term, HOT aims to build an open-source cheap drone optimised for photo capture only, and using open-source &lt;a href="https://github.com/ArduPilot/ardupilot" rel="noopener noreferrer"&gt;ArduPilot&lt;/a&gt; as the flight controller.&lt;/li&gt;
&lt;li&gt;In the short term, expanding drone support with similar cheap drones is critical for adoption.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. The Potensic Atom Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Potensic’s Atom 1 and Atom 2 offer affordable alternatives (~1/3 the cost of DJI), with promising hardware.&lt;/li&gt;
&lt;/ul&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%2Fpd4tqojs6cbf8dknapox.jpeg" 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%2Fpd4tqojs6cbf8dknapox.jpeg" alt="potensic-atom-1" width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
Here we have the Potensic Atom v1. You can clearly see the scuffed blades where I crashed it into a plant pot on landing 😂&lt;/p&gt;
&lt;h3&gt;
  
  
  Comparison
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Atom 1&lt;/strong&gt; : Supports waypoint missions with the current firmware.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Atom 2&lt;/strong&gt; : Technically superior hardware (we suspect is uses the same photo sensor as the DJI Mini 4 Pro), but as of 2025-05-11, it does not yet support waypoint missions. Firmware update is expected later this year.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Note&lt;br&gt;
Potensic currently imposes a 30 waypoint limit per mission in it’s apps. This needs further testing to assess if it’s a hard or soft limit.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  3. Technical Exploration
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Existing Attempts
&lt;/h3&gt;

&lt;p&gt;An &lt;a href="https://github.com/koen-aerts/potdroneflightparser" rel="noopener noreferrer"&gt;excellent community project&lt;/a&gt; by @koen-aerts reverse-engineered the proprietary flight log format for Potensic drones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can visualise flight logs via a Kivy app, replaying the flight parameters and drone location as an interactive movie.&lt;/li&gt;
&lt;li&gt;Offers alpha support for waypoint mission loading/saving, primarily by clicking on a map, so it’s not overly sophisticated (yet).&lt;/li&gt;
&lt;li&gt;See more details on his blog &lt;a href="https://koenaerts.ca/micro-drones/parsing-potensic-flight-data-files" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Waypoint Planning in DroneTM
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;DroneTM already contains a&lt;a href="https://github.com/hotosm/drone-tm/tree/develop/src/backend/packages/drone-flightplan" rel="noopener noreferrer"&gt;flight plan generation module&lt;/a&gt;flight plan generation module, that is used to generate waypoint and wayline missions.&lt;/li&gt;
&lt;li&gt;The primary output candidate is DJI Waypoint Markup Language, however, the remainder of this blog will look into the potential for outputting to a format supported by the Potensic drones.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Storing Waypoints In The Atom
&lt;/h3&gt;
&lt;h3&gt;
  
  
  The &lt;code&gt;map.db&lt;/code&gt; Waypoint File
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The flow to consider here is: &lt;strong&gt;PotensicPro&lt;/strong&gt; Android app (phone) --&amp;gt; &lt;strong&gt;Flight controller&lt;/strong&gt; –&amp;gt; &lt;strong&gt;Atom Drone&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It looks like the PotensicPro stores the waypoint missions in a &lt;code&gt;map.db&lt;/code&gt; SQLite database.&lt;/li&gt;
&lt;li&gt;Upon further exploration, this is a pretty half-baked / unsophisticated approach, but it works! And you will see no complaints from me, because they could very well have added no waypoint support at all!&lt;/li&gt;
&lt;li&gt;The file is located on the phone, reading an writing to it as needed:&lt;code&gt;/data/data/com.ipotensic.potensicpro/databases/map.db&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Note&lt;/p&gt;

&lt;p&gt;Android File Access: A Quick Primer&lt;/p&gt;

&lt;p&gt;An aside on how Android sanboxed filesystem works per-app since Android 14.&lt;/p&gt;

&lt;p&gt;There are two storage options for apps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each app gets it own &lt;strong&gt;sandboxed storage&lt;/strong&gt; to work with, to prevent privileged access to the entire filesystem: &lt;code&gt;/data/data/&amp;lt;package.name&amp;gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Shared storage is also available if permission is granted, under:&lt;code&gt;/storage/emulated/0/Android/data/&amp;lt;package.name&amp;gt;/&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;However:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In order to copy the &lt;code&gt;map.db&lt;/code&gt; file across to a place on the phone that the PotensicPro app can read, we need to copy to this sandboxed filesystem.&lt;/li&gt;
&lt;li&gt;The sandboxed filesystem is only accessible directly from the &lt;strong&gt;app code&lt;/strong&gt; , unless the device is rooted, or the app uses a &lt;code&gt;debuggable&lt;/code&gt; flag in it’s manifest (see more info below).&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;We have two options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Mod the PotensicPro app further, to read &lt;code&gt;map.db&lt;/code&gt; from &lt;strong&gt;shared storage&lt;/strong&gt; instead.&lt;/li&gt;
&lt;li&gt;Use ADB to interact with the sandboxed storage, via ‘run-as’ the required user.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  4. Implementing Waypoint Generation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  An Existing Modded App
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Option 1 above is probably not a maintainable solution into the future.&lt;/li&gt;
&lt;li&gt;There are already modded versions of PotensicPro floating around, with one of the best coming from ‘e443mods’ telegram community:

&lt;ul&gt;
&lt;li&gt;Changes the height, waypoint &amp;amp; distance limits to 10000m.&lt;/li&gt;
&lt;li&gt;Removes unnecessary trackers.&lt;/li&gt;
&lt;li&gt;Changes the transmission mode to the maximum possible.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;I also modded the modded APK 😆 to add the&lt;a href="https://github.com/julKali/makeDebuggable" rel="noopener noreferrer"&gt;debuggable&lt;/a&gt; flag to &lt;strong&gt;AndroidManifest.xml&lt;/strong&gt; , as described above.

&lt;ul&gt;
&lt;li&gt;I’m not sure I can legally host and distribute this, but suffice to say it’s out there.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The process to make an existing APK ‘debuggable’:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Gen a new signing key
docker run -it --rm -v $PWD:/home/makedebuggable -u makedebuggable ghcr.io/spwoodcock/make-debuggable:2025-05-11 /scripts/keygen.sh

# Mod the APK to be debuggable
docker run -it --rm -v $PWD:/home/makedebuggable -u makedebuggable ghcr.io/spwoodcock/make-debuggable:2025-05-11 /scripts/makeDebuggable.py apk PotensicPro-V6.6.1
-E443.apk PotensicPro-V6.6.1-E443-debug.apk debuggable.keystore debuggable pwpwpw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;br&gt;
    &lt;span&gt;&lt;/span&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This allows us to access the sandboxed PotensicPro app files, including the waypoint database.&lt;/li&gt;
&lt;li&gt;Android USB debugging must be enabled via the phone developer options.&lt;/li&gt;
&lt;li&gt;This isn’t overly user friendly, so I will probably look into&lt;a href="https://spwoodcock.dev/blog/2025-05-11-potensic-atom-waypoints/#adb-in-the-browser" rel="noopener noreferrer"&gt;ADB In The Browser&lt;/a&gt; for DroneTM going forward.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Inspecting The Waypoint Database
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The first step was to fly a waypoint mission manually via PotensicPro.&lt;/li&gt;
&lt;li&gt;Some important info before we attempt to get the file:

&lt;ul&gt;
&lt;li&gt;When using &lt;code&gt;run-as com.ipotensic.potensicpro&lt;/code&gt; in ADB we cannot access anything other than the sandboxed filesystem (no shared storage access…).&lt;/li&gt;
&lt;li&gt;When using the standard user, we cannot access the sandboxed PotensicPro storage… a catch-22.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;So, to copy the generated &lt;code&gt;map.db&lt;/code&gt; file from the sandbox onto our filesystem, instead of copying to an intermediary location then using &lt;code&gt;adb pull&lt;/code&gt;, we can pipe the file content directly to our machine via terminal:
&lt;/li&gt;

&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# First ensure the db is not being accessed
./adb shell am force-stop com.ipotensic.potensicpro

# This approach doesn't work due to permission issues
#./adb shell run-as com.ipotensic.potensicpro cp /data/data/com.ipotensic.potensicpro/databases/map.db /sdcard/Android/data/com.ipotensic.potensicpro/files/

# This works (we use base64 encode to avoid encoding issues between shells, e.g. powershell vs bash, see below)
./adb exec-out run-as com.ipotensic.potensicpro base64 /data/data/com.ipotensic.potensicpro/databases/map.db &amp;gt; map.db.b64
# I then copied this across to WSL decoding base64
dos2unix map.db.b64
base64 -d map.db.b64 &amp;gt; map.db

# For a Linux-only workflow, it would be simpler to just:
cat map.db | ./adb exec-in run-as com.ipotensic.potensicpro /data/data/com.ipotensic.potensicpro/databases/map.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;br&gt;
    &lt;span&gt;&lt;/span&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A note about shell encoding… how I wasted 2hrs of my life.&lt;/p&gt;

&lt;p&gt;Originally I was using:&lt;code&gt;./adb exec-out run-as com.ipotensic.potensicpro cat /data/data/com.ipotensic.potensicpro/databases/map.db &amp;gt; map.db&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This failed as I was testing using Powershell instead of bourne / bash. &amp;gt; After some hex inspection I noticed the encoding was wrong in the copied file headers.&lt;/p&gt;

&lt;p&gt;In PowerShell, using redirection (&amp;gt;) causes the new files to be encoded in Windows UTF-16LE encoding, while Unix-like filesystems require UTF-8. &amp;gt; Making the file not a valid SQLite file.&lt;/p&gt;

&lt;p&gt;So I swapped to the base64 encoding approach above, that should be cross compatible between different terminals!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Creating A Waypoint File
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;For now I implemented this as a test only, with hardcoded coordinates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In the long run I will tie this into the existing code in drone-flightplan:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I made a start on this, replicating the SQLite database table structure, and inserting a dummy waypoint flight plus hardcoded coordinates for my flight in London today.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&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%2F5w0i1ekwg2eux0adyh80.jpeg" 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%2F5w0i1ekwg2eux0adyh80.jpeg" alt="london-imagery" width="800" height="672"&gt;&lt;/a&gt;&lt;br&gt;
Out of interest, here is a crappy orthophoto I generated by simply flying around my area and taking some test photos. The output will no doubt be much better using a regular interval consistent bearing of a waypoint flight, but it gives you an idea!&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://spwoodcock.dev/blog/2025-05-11-potensic-atom-waypoints/#stay-tuned" rel="noopener noreferrer"&gt;Stay Tuned&lt;/a&gt; below to follow up on the progress here.&lt;/p&gt;

&lt;h4&gt;
  
  
  Limitations
&lt;/h4&gt;

&lt;p&gt;The SQLite format used isn’t particularly sophisticated. Basically we have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A simple table to reference a mission, with a set height and other metadata.&lt;/li&gt;
&lt;li&gt;A linked table storing lat/lon pairs for each waypoint in the mission.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As a result, three major limitations has been identified:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The mission altitude is set at a single level for the entire flight.&lt;/li&gt;
&lt;li&gt;Photos must be taken using the interval timer feature, as taking a photo at each waypoint cannot be programmed.&lt;/li&gt;
&lt;li&gt;The gimbal angle also cannot be adjusted automatically, meaning two runs of the mission would be required for both nadir (~85°) and oblique (~45°) imagery capture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DJI obviously have an advantage here and clearly have a big team of software engineers at their disposal. The Waypoint Markup Language (WPML) is a very professional and infinitely configurable spec in comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  Importing The Generated File To A Phone
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Copying back to the correct place on a phone:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Cleanup old journal files
./adb shell run-as com.ipotensic.potensicpro rm -f databases/map.db-journal

# Overwrite db
base64 map.db &amp;gt; map.db.b64
# Linux
./adb shell run-as com.ipotensic.potensicpro sh -c "'base64 -d &amp;gt; databases/map.db'" &amp;lt; map.db.b64
# Powershell
Get-Content -Raw "map.db.b64" | ./adb shell run-as com.ipotensic.potensicpro sh -c "'base64 -d &amp;gt; databases/map.db'"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;br&gt;
    &lt;span&gt;&lt;/span&gt;&lt;br&gt;
  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next, PotensicPro should be opened, the waypoint map opened, then on the right there should be an option to view ‘previously flown’ waypoint missions.&lt;/li&gt;
&lt;li&gt;Click on the generated flight, then opt to ‘fly it again’ (in reality, for the first time).&lt;/li&gt;
&lt;li&gt;Note that before starting the mission, the interval timer should be set to an interval of ~2 - 3 second shots, else you will get no imagery!&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ADB In The Browser
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Requiring the user to install and use &lt;code&gt;adb&lt;/code&gt; isn’t user friendly / accessible.&lt;/li&gt;
&lt;li&gt;Asking users to also enable USB debugging also isn’t ideal… but I think this will still be required in any environment we use. It’s not a huge dealbreaker, as it’s a one time only action.&lt;/li&gt;
&lt;li&gt;There is a very interesting ADB implementation in JavaScript called&lt;a href="https://github.com/yume-chan/ya-webadb" rel="noopener noreferrer"&gt;Tango&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;I will definitely be exploring this further and implementing a simple workflow to:

&lt;ul&gt;
&lt;li&gt;Generate flightplans in the DroneTM web app.&lt;/li&gt;
&lt;li&gt;Click ‘transfer to device’.&lt;/li&gt;
&lt;li&gt;Have the flightplan automatically copied to the necessary place to be picked up.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;If Potensic released an SDK then this wouldn’t be necessary. But I would say that is highly unlikely.&lt;/li&gt;

&lt;li&gt;DJI actually recently released an SDK for the Mini 4 Pro, that we will be &lt;a href="https://github.com/hotosm/drone-tm/issues/535" rel="noopener noreferrer"&gt;integrating into our DroneTM workflow soon&lt;/a&gt;!&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stay Tuned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://spwoodcock.dev/blog/2025-05-11-potensic-atom-waypoints/#creating-a-waypoint-file" rel="noopener noreferrer"&gt;creating a waypoint file&lt;/a&gt; section doesn’t include all the info for brevity, and because the work is ongoing 😄&lt;/li&gt;
&lt;li&gt;Follow progress on the implementation in this&lt;a href="https://github.com/hotosm/drone-tm/pull/545" rel="noopener noreferrer"&gt;linked pull request&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;I will be testing the generated waypoint files work on probably the weekend 17th May.&lt;/li&gt;
&lt;li&gt;From there, it will be a simple matter of hooking up the waypoint generation logic, and adding it as a drone option in DroneTM.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>hardware</category>
      <category>hacking</category>
      <category>drones</category>
    </item>
    <item>
      <title>Programming languages in 2025: what we have now and future predictions</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Tue, 22 Apr 2025 00:00:00 +0000</pubDate>
      <link>https://dev.to/spwoodcock/programming-languages-in-2025-what-we-have-now-and-future-predictions-9hf</link>
      <guid>https://dev.to/spwoodcock/programming-languages-in-2025-what-we-have-now-and-future-predictions-9hf</guid>
      <description>&lt;h2&gt;
  
  
  So What Languages Do We Have: Backend
&lt;/h2&gt;

&lt;p&gt;Right off the bat:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I am coming at this with an extreme bias for web development.&lt;/li&gt;
&lt;li&gt;Many people may disagree with my assessments, particularly if not focusing on web development.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Java
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;I won’t dig into this too much.&lt;/li&gt;
&lt;li&gt;Java &lt;strong&gt;was&lt;/strong&gt; a great language up to mid 2010’s.&lt;/li&gt;
&lt;li&gt;Now there are many more objectively better alternatives, when it comes to anything web related at least.&lt;/li&gt;
&lt;li&gt;For many modern web services, the Java Virtual Machine (JVM) feels like an unnecessary abstraction and overkill. It consumes a lot of memory, has higher startup times, and is generally a bit inefficient.&lt;/li&gt;
&lt;li&gt;Other languages such as Golang could replace this easily, with easier syntax and better performance.&lt;/li&gt;
&lt;li&gt;Usage is in decline, but Java still has a huge market and foothold however (it’s used lot in enterprise, high-performance computing, financial services).&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Kotlin&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kotlin complicates this a bit - it’s sparked new interest in the JVM world.&lt;/p&gt;

&lt;p&gt;It improves on Java with more concise syntax, better null safety, data classes, and more.&lt;/p&gt;

&lt;p&gt;That said, it still runs on the JVM…&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  C, C++
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;C&lt;/strong&gt; is the current standard for low-level performant critical code.&lt;/li&gt;
&lt;li&gt;It can run anywhere: embedded systems, backend web API, etc.&lt;/li&gt;
&lt;li&gt;It’s also dominant in systems programming, embedded development, and OS kernels.&lt;/li&gt;
&lt;li&gt;We can also bind to C from any other language easily, making it a great candidate for low-level ‘write-once, bind-many’ libraries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C++&lt;/strong&gt; is an extension of C with other advanced features and more capabilities like an Object-Oriented model, templates, and RAII.&lt;/li&gt;
&lt;li&gt;Many low-level libraries are written in C++, for example the excellent GEOS library for processing geospatial vector data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Rust
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Rust is a C and C++ replacement, with key advantages being &lt;strong&gt;memory safety&lt;/strong&gt; , no garbage collector, better concurrency, enforced type safety, and no runtime requirement (C++ needs a runtime, while Rust is entirely compiled to native machine code).&lt;/li&gt;
&lt;li&gt;While we have a huge amount of performance critical libs written in C and C++, any &lt;strong&gt;future&lt;/strong&gt; libraries should probably be written in Rust.&lt;/li&gt;
&lt;li&gt;While it’s possible to bind to C/C++ libs from Rust, ideally existing libraries will be ported to Rust into the future.

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://github.com/georust" rel="noopener noreferrer"&gt;georust&lt;/a&gt; community is hard at work replacing many low level geospatial libraries in Rust, such as GEOS.&lt;/li&gt;
&lt;li&gt;Maybe osgeo/GDAL one day?&lt;/li&gt;
&lt;li&gt;Replacing SQLite C library with &lt;a href="https://github.com/tursodatabase/limbo" rel="noopener noreferrer"&gt;Limbo&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Golang
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Rust is a “better” language when it comes to performance and code quality.&lt;/li&gt;
&lt;li&gt;The key tradeoff here is simplicity: Golang goes &lt;strong&gt;most of the way&lt;/strong&gt; to being an excellent performant modern programming language, while keeping concepts reasonably simple and syntax easy to read / write.&lt;/li&gt;
&lt;li&gt;Go has a very good standard library built in, and is excellent at networking.&lt;/li&gt;
&lt;li&gt;Huge tools like Kubernetes and the ecosystem around containers are all written in Go, so it’s no going anywhere soon!&lt;/li&gt;
&lt;li&gt;If I had to write a small microservice / web API quickly and easily maintainable, I would use Golang. I did just that in a &lt;a href="https://github.com/hotosm/central-webhook" rel="noopener noreferrer"&gt;recent project&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Python
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Python is a fantastic high-level language with a huge community.&lt;/li&gt;
&lt;li&gt;It was the first language that I learned.&lt;/li&gt;
&lt;li&gt;It’s continually evolving and improving, moving from its status as a simple scripting language, to running many much more serious applications these days:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Web APIs&lt;/strong&gt; with acceptable performance for most.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;machine learning&lt;/strong&gt; community has consolidated around Python.&lt;/li&gt;
&lt;li&gt;Anything else you can think of!&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;The introduction of type hinting and other improvements from other languages have ensured Pythons spot at the top of the easy-to-use / modern and maintainable tradeoff list.&lt;/li&gt;

&lt;li&gt;For writing actual applications that leverage low-level languages underneath, or require building a community around them, Python is still my go-to language (for now).&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where Does Frontend Fit In?
&lt;/h2&gt;

&lt;p&gt;There are two programming languages available in web browsers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript&lt;/strong&gt; / &lt;strong&gt;TypeScript&lt;/strong&gt; : what the web is made of currently (alongside HTML/CSS).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebAssembly&lt;/strong&gt; : this is pretty much ‘backend code, in frontend’!&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  JavaScript… On The Backend??
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NodeJS&lt;/strong&gt; introduced JavaScript for backend programming.&lt;/li&gt;
&lt;li&gt;We have seen the rise of many full integrated libraries covering both backend and frontend for web development, such as SvelteKit, Nest, Nuxt, etc.&lt;/li&gt;
&lt;li&gt;We have also seen new JS engines, such as &lt;strong&gt;Deno&lt;/strong&gt; and &lt;strong&gt;Bun&lt;/strong&gt; , to fix some of the underlying issues with NodeJS.&lt;/li&gt;
&lt;li&gt;But without going too deep, JavaScript just isn’t a great backend language. It’s single-threaded (aside from worker threads), uses prototype-based inheritance, and leans heavily on callbacks - all of which can complicate large codebases.&lt;/li&gt;
&lt;li&gt;TypeScript solves the weak typing problem, but that only goes so far.&lt;/li&gt;
&lt;li&gt;As an alternative, I would advocate for:

&lt;ul&gt;
&lt;li&gt;Simple web apps: use Python or Golang to write a simple HTMX server.&lt;/li&gt;
&lt;li&gt;More complex web apps, requiring real-time or instant responsiveness / reactivity: use a &lt;a href="https://localfirstweb.dev" rel="noopener noreferrer"&gt;local-first&lt;/a&gt; web development approach (future blog about this!), for offline-ready apps that sync directly with a database.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;JavaScript vs Python&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These days it’s a really tough call for which one to focus on.&lt;/p&gt;

&lt;p&gt;Python is easy and has a large community. It also has many niche applications where it beats JS (like machine learning libraries).&lt;/p&gt;

&lt;p&gt;However, JavaScript is equally capable, while being useful for development across the entire stack. JavaScript is also catching up on the usage of models directly in-browser, with potential for model generation done entirely in-browser too! Check out &lt;a href="https://github.com/huggingface/transformers.js" rel="noopener noreferrer"&gt;transformers.js&lt;/a&gt;and &lt;a href="https://github.com/microsoft/onnxruntime" rel="noopener noreferrer"&gt;onnxruntime&lt;/a&gt; if interested.&lt;/p&gt;

&lt;p&gt;If you are focusing on local-first web development, then using JavaScript is probably your primary concern.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  WebAssembly
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Typically WebAssembly isn’t written directly, but instead one of the backend languages listed above are &lt;strong&gt;compiled&lt;/strong&gt; into WebAssembly.&lt;/li&gt;
&lt;li&gt;Out of all the awesome backend languages I listed above, these are the primary candidates:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;C/C++&lt;/strong&gt; : emscripten can do this, but we introduce two problems:

&lt;ol&gt;
&lt;li&gt;Interfacing directly with the library from JavaScript means handling memory usage carefully and is not very user friendly.&lt;/li&gt;
&lt;li&gt;We write a user-friendly wrapper for the low-level API, that has to be maintained into the future.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust&lt;/strong&gt; : was early to adopt WebAssembly as a first-class target and provides excellent tooling for compiling to it. It handles the issues around memory safety, meaning we can write our library in Rust, compile to WebAssembly, and essentially start using it directly in the web.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Golang&lt;/strong&gt; : can also compile to WebAssembly and is a pretty decent, but less performant than Rust. The main problem is that it’s a garbage-collected language, meaning lots of superfluous code also needs to be compiled alongside your code (&lt;code&gt;wasm_exec.js&lt;/code&gt; adds ~300kb). If doing this, I would recommend &lt;a href="https://github.com/tinygo-org/tinygo" rel="noopener noreferrer"&gt;TinyGo&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

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

&lt;p&gt;What to write (or learn) in 2025 and going forward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rust&lt;/strong&gt; : low level systems programming, or performant library to bind from higher level languages, including WebAssembly to run in the web. Complex &amp;amp; requires skill to use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Golang&lt;/strong&gt; : willing to sacrifice some of the benefits of Rust for speed and ease of development. Ideal for small projects, startups, demos, microservices, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt; : if writing simple websites (most of the web!) that could use the simplicity of HTMX. Or if working with machine learning / modelling.  Or for writing &lt;strong&gt;scripts&lt;/strong&gt;, where Python really shines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript&lt;/strong&gt; : if you are writing user-facing application, this is the language to learn. Particularly if you wish to make a web app that has a very modern feel and leverages the advantages of local-first web development.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  As Promised, Some Prediction
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;C &amp;amp; C++ usage will eventually decline in favour of Rust, for &lt;strong&gt;low-level performant libraries&lt;/strong&gt;.
A good example could be the migration of GEOS (C) to GeoRust (Rust). I'm far from the first to
point this out - it’s a growing trend (even the Linux kernel may have some Rust modules...).&lt;/li&gt;
&lt;li&gt;The web ecosystem is &lt;strong&gt;diverging&lt;/strong&gt; from the traditional 'backend' API-driven model toward
&lt;strong&gt;local-first JavaScript&lt;/strong&gt; approaches (compute and data in the browser).&lt;/li&gt;
&lt;li&gt;Python usage will remain high, but JavaScript will dominate further in the web
domain, as backend APIs (± ML / LLM) are swapped in favour of &lt;strong&gt;WASM compiled libraries&lt;/strong&gt;
directly in the frontend
(&lt;a href="https://github.com/electric-sql/pglite" rel="noopener noreferrer"&gt;Postgres in the browser&lt;/a&gt; anyone?).&lt;/li&gt;
&lt;li&gt;Personally, I think a safe bet would be to &lt;strong&gt;double down on the JS / Rust combo&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Bonus: Other Languages&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Elixir&lt;/strong&gt; : tightly integrated with web development and particularly well-suited to high-concurrency applications like real-time collaboration tools due to it’s BEAM VM usage. Electric-SQL is built with this, and I love it!&lt;/li&gt;
&lt;li&gt;I didn’t mention unrelated languages, such as &lt;strong&gt;Swift&lt;/strong&gt; for iOS development, &lt;strong&gt;Kotlin&lt;/strong&gt; for Android development, and other JVM-based languages like &lt;strong&gt;Scala&lt;/strong&gt; or &lt;strong&gt;Clojure&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Bonus: A Note About Systems Programming&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In an ideal world, much legacy C/C++ could be migrated to Rust. However, this isn’t a realistic goal, throwing away years of work on open-source projects and entire communities.&lt;/li&gt;
&lt;li&gt;New projects should ideally be written in Rust, and migration efforts can be attempted.&lt;/li&gt;
&lt;li&gt;There have been some emerging systems languages that help to improve C or C++ but building on existing tooling and code to help a gradual migration.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;C&lt;/strong&gt; --&amp;gt; &lt;strong&gt;Zig&lt;/strong&gt; (simpler build system, better C interop, no hidden control flow).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C++&lt;/strong&gt; --&amp;gt; &lt;strong&gt;Carbon&lt;/strong&gt; (Google-backed attempt to evolve C++ with modern tooling).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;This is a slight oversimplification, but mostly covers it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

</description>
      <category>programming</category>
      <category>future</category>
      <category>webdev</category>
      <category>webassembly</category>
    </item>
    <item>
      <title>Building an open-source community at HOTOSM: a thank you</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Sun, 09 Mar 2025 04:07:27 +0000</pubDate>
      <link>https://dev.to/spwoodcock/building-an-open-source-community-at-hotosm-a-thank-you-41f</link>
      <guid>https://dev.to/spwoodcock/building-an-open-source-community-at-hotosm-a-thank-you-41f</guid>
      <description>&lt;p&gt;I’m deeply embedded in the open-source humanitarian software space by now.&lt;/p&gt;

&lt;p&gt;This post is a retrospective of my time spent here so far, and a highlight of the successes from the open-source community at HOTOSM.&lt;/p&gt;

&lt;p&gt;I apologise in advance if its overly long and bit rambly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This All Began
&lt;/h2&gt;

&lt;p&gt;I started working with HOTOSM as a volunteer in Feb 2022. They needed urgent assistance with developing a new tool called the Field Mapping Tasking Manager, in response to the Turkey / Syria earthquake crisis at the time.&lt;/p&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%2Fgliclytispsgr1ompgqk.jpg" 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%2Fgliclytispsgr1ompgqk.jpg" alt="fmtm-call-to-action" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Having worked on open-source tools for the preceding two years, I was pretty keen to volunteer my time.&lt;/p&gt;

&lt;p&gt;After speaking with their senior humanitarian advisor (and now a great friend), &lt;a href="https://ivangayton.net" rel="noopener noreferrer"&gt;Ivan Gayton&lt;/a&gt;, I was quickly convinced about the potential for the tool, and took a week off work to push forward developments.&lt;/p&gt;

&lt;p&gt;Fast forward a few years and I am now the senior technical lead at HOTOSM, coordinating and assisting the development of our entire &lt;a href="https://www.hotosm.org/tech-suite.html" rel="noopener noreferrer"&gt;suite of tools&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Governance&lt;/p&gt;

&lt;p&gt;HOT never fully formalised a governance model, but operates a do-ocracy for the most part.&lt;br&gt;
The main contributors have the most input for the direction of a tool!&lt;/p&gt;

&lt;p&gt;Of course, this doesn't always hold true, particularly when hiring contractors&lt;br&gt;
to work on specific features with a pre-determined job spec.&lt;/p&gt;

&lt;p&gt;But HOT does try to be extremely transparent with it's approach, working in the&lt;br&gt;
open, and actively encouraging input from the community.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Community
&lt;/h2&gt;

&lt;p&gt;Community has always been HOT’s strongest asset. I am by no means the expert on this topic.&lt;/p&gt;

&lt;p&gt;But I do know that the community largely formed around &lt;a href="https://tasks.hotosm.org" rel="noopener noreferrer"&gt;Tasking Manager&lt;/a&gt;, responding to humanitarian activations for disaster response and resiliance.&lt;/p&gt;

&lt;p&gt;Sub-communities formed around procuring and processing base imagery, in-person mapathon events globally, country-wide communities deeply rooted in OpenStreetMap and other open communities such as OSGeo and YouthMappers.&lt;/p&gt;

&lt;p&gt;The global reach of the community is truly astounding, with ~5000 users registered on&lt;a href="https://slack.hotosm.org" rel="noopener noreferrer"&gt;Slack channels&lt;/a&gt; alone.&lt;/p&gt;

&lt;p&gt;HOT also has a large network of software development collaborators, with names such as DevelopmentSeed and Kontur frequently stepping in to assist.&lt;/p&gt;

&lt;p&gt;While our mapping and data volunteer network is strong, we still struggle to cultivate a sustainable open-source software development community. Why is that? Would a different governance model help?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note&lt;/p&gt;

&lt;p&gt;HOT recently joined the the Cloud Native Geospatial Foundation,&lt;br&gt;
something I'm particularly excited about having followed their work for so long!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Software Development Skills Are Truly Global
&lt;/h2&gt;

&lt;p&gt;A degree from a top university doesn’t automatically make someone a great developer. Real-world problem-solving, especially in humanitarian tech, demands a broader skill set — one that prioritises practicality over algorithmic challenges.&lt;/p&gt;

&lt;p&gt;Sure you can produce exceptionally crafted algorithms, but are the end users needs best served by this? Having rarely lived through hardship or a humanitarian crisis, is our mindset suitably aligned to meet the multi-faceted requirements? (Of course, I realize the irony here — this probably exposes my own biases too). This is especially prescient in an often resource-constrained humanitarian sector.&lt;/p&gt;

&lt;p&gt;I have worked with a mixed bag of developers, many of which don’t immediately grasp the bigger picture of what they are developing. When funneling $$$ into AWS isn’t always an option, thinking outside the box may be required. Sometimes simple (and low resource, low budget) is best.&lt;/p&gt;

&lt;p&gt;It also often requires understanding the full stack implications of what you are developing: delivering tools from end to end, considering development time, ease of understanding and maintainability, plus hosting costs in the long term (factoring possible scaling too).&lt;/p&gt;

&lt;p&gt;Where I am going with this is that the top coding talent need not be constrained to the talent pool from software engineer grads from the “Western world”. Hiring for projects in this sector has been an eye opening experience. The rest of the world is getting on fine with some excellent software developers. You just need to know where to look.&lt;/p&gt;

&lt;p&gt;Case in point to this is the burgeoning geospatial software industry based in Kathmandu, Nepal. One of the most talented and ‘going-somewhere’ engineers I have met is my friend and colleague&lt;a href="https://www.hotosm.org/updates/staff-spotlight-series-kshitij-sharma" rel="noopener noreferrer"&gt;Kshitij Sharma&lt;/a&gt;, who graduated from the excellent Institute of Engineering at Tribhuvan University.&lt;/p&gt;

&lt;p&gt;HOT’s biggest tech partner is the organisation &lt;a href="https://naxa.com.np" rel="noopener noreferrer"&gt;NAXA&lt;/a&gt;, a group that is truly a pioneer of the geospatial development industry, working alongside many, many international partners to achieve theirs goals.&lt;/p&gt;

&lt;p&gt;From this partnership, &lt;a href="https://dronetm.org" rel="noopener noreferrer"&gt;DroneTM&lt;/a&gt; was conceived, a tool for community driven drone imagery collection - with the vast majority of the technical challenges being solved by the team based in Nepal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notable Contributions
&lt;/h2&gt;

&lt;p&gt;Since working with HOT, I have had some excellent community contribution experiences regarding software development.&lt;/p&gt;

&lt;p&gt;Here are a couple of examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  HOTOSM UI
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/joltcode" rel="noopener noreferrer"&gt;Joe&lt;/a&gt; has been invaluable to the team in the past few years.&lt;/li&gt;
&lt;li&gt;Not only did he hash out the design choices taken for the &lt;a href="https://github.com/hotosm/ui" rel="noopener noreferrer"&gt;hotosm/ui&lt;/a&gt; project, a web component library that will be integrated into all of our tools into the future.&lt;/li&gt;
&lt;li&gt;But Joe has also contributed many &lt;strong&gt;large&lt;/strong&gt; patches to both Tasking Manager and FieldTM, overhauling our build tools, migrating the entire Tasking Manager codebase from JavaScript --&amp;gt; TypeScript, and more 🙌&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Simple Reverse Geocoding
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/emirfabio" rel="noopener noreferrer"&gt;Emir&lt;/a&gt; worked on this fantastic self-contained project over a few months, with the outcome being a very professional and comprehensive solution.&lt;/li&gt;
&lt;li&gt;The goal was simple: a basic &lt;a href="https://github.com/hotosm/pg-nearest-city" rel="noopener noreferrer"&gt;reverse-geocoder based on PostGIS&lt;/a&gt;, that required no external dependencies.&lt;/li&gt;
&lt;li&gt;The design pattern used for sync/async Python code can be carried across to future projects too!&lt;/li&gt;
&lt;li&gt;Emir is currently working on the final extension of the project: internationalised city name output.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  GeoJSON AOI Parsing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.linkedin.com/in/luke-everhart-ab6277211" rel="noopener noreferrer"&gt;Luke&lt;/a&gt;, a volunteer who recently reached out, is working on a small idea I had a while ago called&lt;a href="https://github.com/hotosm/geojson-aoi-parser" rel="noopener noreferrer"&gt;geojosn-aoi-parser&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Pretty much all of our tools do some sort of user-provided geojson parsing.&lt;/li&gt;
&lt;li&gt;The plan with this module is to standardise the parsing to a consistent format, while again only relying on PostGIS as a dependency (all of our tools use this).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  FMTM Test Cases
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/azharcodeit" rel="noopener noreferrer"&gt;Azhar&lt;/a&gt; was an &lt;a href="https://www.outreachy.org" rel="noopener noreferrer"&gt;Outreachy&lt;/a&gt; intern who helped with creating many test cases for the FieldTM backend.&lt;/li&gt;
&lt;li&gt;This has paid dividends for the stability of the API into the future.&lt;/li&gt;
&lt;li&gt;She also assisted with the DroneTM flight plan generation module, adding support for arbitrary rotation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Others
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Charlie&lt;/strong&gt;, who assisted the brainstorming for a new data conflation workflow in FieldTM (which we haven’t managed to see to completion yet, sorry!)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ozgur&lt;/strong&gt;, who has been assisting us with the development of a PowerBI dashboard for managers of field data collection campaigns.&lt;/li&gt;
&lt;li&gt;On top of these notable contributions I have been present for, there have been many more (particularly for Tasking Manager), which I haven’t mentioned: productive discussions, filing bug reports, making PRs - it all counts!&lt;/li&gt;
&lt;li&gt;All of the brainstorming and collaboration we have done with the ODK team and community to date! This was touched on in&lt;a href="https://spwoodcock.dev/blog/2024-06-contributing-to-odk" rel="noopener noreferrer"&gt;another blog post&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Call To Action
&lt;/h2&gt;

&lt;p&gt;With a funding crisis in the humanitarian sector currently - from COVID, to global instability, to the downright unethical closing of USAID - the purse strings are continually tightening.&lt;/p&gt;

&lt;p&gt;We need your contributions more than ever! Find our how you can best contribute &lt;a href="https://docs.hotosm.org/become-a-contributor" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feel free to reach out to member of the Tech Team through any channel. Let’s grow HOT’s humanitarian tech community one new contributor at a time 💪&lt;/p&gt;

</description>
    </item>
    <item>
      <title>All About CORS Headers</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Wed, 05 Mar 2025 15:56:42 +0000</pubDate>
      <link>https://dev.to/spwoodcock/all-about-cors-headers-44jb</link>
      <guid>https://dev.to/spwoodcock/all-about-cors-headers-44jb</guid>
      <description>&lt;p&gt;There are already a lot of resources about CORS out there.&lt;/p&gt;

&lt;p&gt;I’m going to try and keep this short and sweet — useful tips for web devs.&lt;/p&gt;

&lt;h2&gt;
  
  
  About CORS
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;CORS is a security mechanism based on HTTP headers.&lt;/li&gt;
&lt;li&gt;CORS must be configured on the web &lt;strong&gt;server&lt;/strong&gt; and consumed by the &lt;strong&gt;browser&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It is only relevant for web browsers, not requests from other software such as cURL or a Python script (which will ignore the configuration).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why Does It Exist?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You are on a website with &lt;code&gt;mydomain.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;All web browsers have a Same-Origin-Policy (SOP), preventing access to domains such as &lt;code&gt;sub.mydomain.com&lt;/code&gt;or &lt;code&gt;otherdomain.com&lt;/code&gt;, as a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy" rel="noopener noreferrer"&gt;security measure&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Cross-Origin Resource Sharing (CORS) is a mechanism to allow exceptions to this rule.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When Is It Triggered?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;If the schema, hostname, or port do not match, this is considered cross-origin.&lt;/li&gt;
&lt;li&gt;An example would be &lt;code&gt;https://mysite.com&lt;/code&gt; and &lt;code&gt;https://api.mysite.com&lt;/code&gt;, which are different domains and hence cross-origin.&lt;/li&gt;
&lt;li&gt;To avoid CORS for backend APIs, a common approach is to host the API under a subpath instead &lt;code&gt;https://mysite.com/api&lt;/code&gt;, and avoid CORS concerns entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configuring CORS Correctly
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pre-Flight Requests
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Browsers send a “pre-flight” request under certain conditions before making the actual request (for cross-origin requests).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A pre-flight request is a special &lt;code&gt;OPTIONS&lt;/code&gt; request that checks the browser is allowed to submit to the API before making the actual request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As above, to avoid CORS and having to send &lt;strong&gt;two&lt;/strong&gt; requests per API call, it’s possible to host an API under a subpath instead.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Access-Control Headers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;These headers control how CORS is &lt;strong&gt;enforced by the browser&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;They are set on the &lt;strong&gt;web server&lt;/strong&gt; of the resource you are trying to access, e.g. your NGINX proxy or backend API.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Access-Control-Allow-Origins
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Which origins in the browser can call the API / webserver endpoints.&lt;/li&gt;
&lt;li&gt;Examples:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; (allow all access)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Access-Control-Allow-Origin: https://example.com&lt;/code&gt;(specific protocol (https), domain (example.com), and port (443)))&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  Access-Control-Allow-Credentials
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If cookies or HTTP basic auth should be sent with cross-origin requests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example: &lt;code&gt;Access-Control-Allow-Credentials: true&lt;/code&gt; (allow).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Access-Control-Allow-Methods
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Specifies which HTTP methods are permitted for cross-origin requests.&lt;/li&gt;
&lt;li&gt;Example: &lt;code&gt;Access-Control-Allow-Methods: GET, POST, PUT, DELETE&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Access-Control-Expose-Headers
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Specifies which headers the &lt;strong&gt;browser&lt;/strong&gt; can access from the &lt;strong&gt;response&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Example: &lt;code&gt;Access-Control-Expose-Headers: Content-Length,Content-Range&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A good example for this is the Content-Disposition which may contain the original filename of an attachment sent by the server:

&lt;ul&gt;
&lt;li&gt;For the filename: &lt;code&gt;Content-Disposition: attachment; filename="myphoto.jpg"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Allow the header to be read: &lt;code&gt;Access-Control-Expose-Headers: Content-Disposition&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The browser can now download the file with the original filename.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Other Headers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Cross-Origin-Resource-Policy (CORP): controls how strict the CORS policy is:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;same-origin&lt;/code&gt;: Only allow requests from the exact same origin.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;same-site&lt;/code&gt;: Allow requests from subdomains (e.g. api.example.com for example.com).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cross-origin&lt;/code&gt;: Allow all requests from any domain.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Cross-Origin-Opener-Policy (COOP) &amp;amp; Cross-Origin-Embedder-Policy (COEP): typically used for security and performance optimizations. A common example is usage with the Origin Private File System (OPFS) and SharedArrayBuffer.&lt;/li&gt;

&lt;li&gt;Access-Control-Max-Age: defines how long the results of a pre-flight request can be cached before needing to run again.&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>http</category>
    </item>
    <item>
      <title>Combining sync and async Python code: writing a DRY package</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Tue, 18 Feb 2025 14:13:06 +0000</pubDate>
      <link>https://dev.to/spwoodcock/how-to-write-a-dry-combined-sync-async-python-package-4kj8</link>
      <guid>https://dev.to/spwoodcock/how-to-write-a-dry-combined-sync-async-python-package-4kj8</guid>
      <description>&lt;h2&gt;
  
  
  Async Python
&lt;/h2&gt;

&lt;p&gt;Directly from the Python docs:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;asyncio is a library to write concurrent code using the async/await syntax.&lt;/p&gt;

&lt;p&gt;asyncio is used as a foundation for multiple Python asynchronous frameworks that provide high-performance network and web-servers, database connection libraries, distributed task queues, etc.&lt;/p&gt;

&lt;p&gt;asyncio is often a perfect fit for IO-bound and high-level structured network code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A typical use case for asynchronous code is web servers based on the ASGI-spec, such as FastAPI, LiteStar, etc.&lt;/p&gt;

&lt;p&gt;However, sync and async code do not play nicely together.&lt;/p&gt;

&lt;p&gt;Often, we need an async library, but there isn't one available, or vice versa.&lt;/p&gt;

&lt;p&gt;This article will discuss a simple pattern for &lt;strong&gt;writing Python packages compatible with both Synchronous and Asynchronous code&lt;/strong&gt;, while only having to write the functionality once! (no separate libraries)&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining Sync and Async Code
&lt;/h2&gt;

&lt;p&gt;First I will briefly describe the issues with mixing these two paradigms.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Async Works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Each Python interpreter runs in a &lt;strong&gt;process&lt;/strong&gt; on the system.&lt;/li&gt;
&lt;li&gt;Each Python process has an &lt;strong&gt;event loop&lt;/strong&gt; that can run async code.&lt;/li&gt;
&lt;li&gt;An event loop can run multiple pieces of code (&lt;strong&gt;coroutines&lt;/strong&gt;) &lt;a href="https://en.wikipedia.org/wiki/Concurrency_(computer_science)" rel="noopener noreferrer"&gt;concurrently&lt;/a&gt;, awaiting the return of something while another piece of code is executing.&lt;/li&gt;
&lt;li&gt;This has huge benefits for IO-bound tasks, such as downloading multiple files or sending simultaneous database queries.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Sync From Async
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Doing this will typically block the event loop for executing other async code.&lt;/li&gt;
&lt;li&gt;This means that when you hit a piece of synchronous code, you essentially block everything below it from running until it completes, negating most of the benefits of async.&lt;/li&gt;
&lt;li&gt;It also means that if async tasks were started prior to the sync code running, the execution of these tasks will not proceed until the sync code  allows the event loop to run again. This problem is compounded by CPU-heavy tasks, which do not yield control back to the event loop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Async From Sync
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Synchronous code cannot use the &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; keywords, hence async code cannot be executed in it's normal manner.&lt;/li&gt;
&lt;li&gt;To get around this, async code can be run in a newly spawned event loop (&lt;code&gt;asyncio.run()&lt;/code&gt; or &lt;code&gt;loop.run_until_complete()&lt;/code&gt;), a separate thread (&lt;code&gt;ThreadpoolExecutor&lt;/code&gt;) or process (&lt;code&gt;ProcessPoolExecutor&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Doing this significantly complicates codes (possibly introducing threading issues, multiple loops, and unexpected blocking), while providing none of the benefits of having async code in the first place.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How To Write A Package That Is Both Sync / Async
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Always write async first!&lt;/li&gt;
&lt;li&gt;It's much easier to remove the async specific tokens from code using a tokeniser script.&lt;/li&gt;
&lt;li&gt;Once we have our asynchronous implementation, we can use a script to convert it into a synchronous equivalent.&lt;/li&gt;
&lt;li&gt;This way you can release a package with both implementations available to your users: use the correct implementation for your use case.&lt;/li&gt;
&lt;li&gt;There is a nice package called &lt;a href="https://github.com/python-trio/unasync" rel="noopener noreferrer"&gt;unasync&lt;/a&gt; that can do this for you, but the simplest and cleanest implementation I have found was in the &lt;a href="https://github.com/encode/httpcore/blob/master/scripts/unasync.py" rel="noopener noreferrer"&gt;&lt;strong&gt;encode/httpcore&lt;/strong&gt; package&lt;/a&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!venv/bin/python
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pprint&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pprint&lt;/span&gt;

&lt;span class="n"&gt;SUBS&lt;/span&gt; &lt;span class="o"&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;from .._backends.auto import AutoBackend&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;from .._backends.sync import SyncBackend&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;import trio as concurrency&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;from tests import concurrency&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;AsyncIterator&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;Iterator&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;Async([A-Z][A-Za-z0-9_]*)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\2&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;async def&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;def&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;async with&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;with&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;async for&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;for&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;await &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="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;handle_async_request&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;handle_request&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;aclose&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;close&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;aiter_stream&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;iter_stream&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;aread&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;read&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;asynccontextmanager&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;contextmanager&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;__aenter__&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;__enter__&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;__aexit__&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;__exit__&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;__aiter__&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;__iter__&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;@pytest.mark.anyio&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="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;@pytest.mark.trio&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="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;AutoBackend&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;SyncBackend&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;COMPILED_SUBS&lt;/span&gt; &lt;span class="o"&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;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(^|\b)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;regex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;($|\b)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;repl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repl&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SUBS&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;USED_SUBS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&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;unasync_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COMPILED_SUBS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;old_line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;
        &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;old_line&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;USED_SUBS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&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;line&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unasync_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&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;as&lt;/span&gt; &lt;span class="n"&gt;out_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readlines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unasync_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;out_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&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;unasync_file_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;out_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;in_line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readlines&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;out_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readlines&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
                &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;unasync_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;out_line&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;expected&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;unasync mismatch between &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;in_path&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; and &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;out_path&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;'&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;Async code:         &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;in_line&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;'&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;Expected sync code: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;'&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;Actual sync code:   &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;out_line&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unasync_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_only&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dirpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dirnames&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filenames&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_dir&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;filenames&lt;/span&gt;&lt;span class="p"&gt;:&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;filename&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.py&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;rel_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;relpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dirpath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;in_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;in_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&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="n"&gt;in_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;out_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&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="n"&gt;out_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&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="n"&gt;in_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-&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;out_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;check_only&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;unasync_file_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_path&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="nf"&gt;unasync_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_path&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;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;check_only&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--check&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;
    &lt;span class="nf"&gt;unasync_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;httpcore/_async&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;httpcore/_sync&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_only&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;check_only&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;unasync_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tests/_async&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;tests/_sync&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check_only&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;check_only&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;USED_SUBS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&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;SUBS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;unused_subs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SUBS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&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;SUBS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;USED_SUBS&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;These patterns were not used:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;pprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unused_subs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__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;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All credit to the devs at &lt;a href="https://github.com/encode" rel="noopener noreferrer"&gt;encode&lt;/a&gt; for the implementation.&lt;/p&gt;

&lt;p&gt;Without digging into the code too deeply, it should be quite obvious from the &lt;code&gt;SUBS&lt;/code&gt; param what this script does - converting async syntax to sync syntax.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrating Unasync.py
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Add the above &lt;code&gt;unasync.py&lt;/code&gt; code to your repo.&lt;/li&gt;
&lt;li&gt;Place your async code in a &lt;code&gt;_async&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;Modify the &lt;code&gt;SUBS&lt;/code&gt; param and &lt;code&gt;unasync_dir&lt;/code&gt; usage in &lt;code&gt;main()&lt;/code&gt; to match your project structure.&lt;/li&gt;
&lt;li&gt;Run the script to generate the sync code equivalent in the &lt;code&gt;_sync&lt;/code&gt; directory.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I followed this exact approach in a recent project I worked on with a volunteer at HOTOSM (&lt;a href="https://www.linkedin.com/in/emir-fabio-cognigni-4222a1216" rel="noopener noreferrer"&gt;Emir&lt;/a&gt;, an excellent dev!).&lt;/p&gt;

&lt;p&gt;We were looking at a nice approach for making the package available for both sync and async (FastAPI) use cases, and the solution was surprisingly simple, but poorly documented online!&lt;/p&gt;

&lt;p&gt;The full project an implementation can be viewed &lt;a href="https://github.com/hotosm/pg-nearest-city" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Optional: Use Via Pre-Commit Hook
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;In the linked project above, I set a pre-commit hook to trigger the &lt;code&gt;unasync.py&lt;/code&gt; script on each commit.&lt;/li&gt;
&lt;li&gt;This means the synchronous code never gets out of sync (ha!) with the asynchronous code.&lt;/li&gt;
&lt;li&gt;The config for the hook was:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Unasync: Convert async --&amp;gt; sync&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;unasync&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;unasync-all&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python unasync.py&lt;/span&gt;
        &lt;span class="na"&gt;always_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>python</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>async</category>
    </item>
    <item>
      <title>How to Migrate Plex from Windows to Linux: What Most Guides Miss</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Sun, 09 Feb 2025 16:10:13 +0000</pubDate>
      <link>https://dev.to/spwoodcock/migrating-plex-from-windows-to-linux-the-missing-pieces-1do8</link>
      <guid>https://dev.to/spwoodcock/migrating-plex-from-windows-to-linux-the-missing-pieces-1do8</guid>
      <description>&lt;h2&gt;
  
  
  Home Server Overhaul
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I am in the process of migrating from a hybrid Windows / HyperV Ubuntu home server setup, to a Debian-based Kubernetes cluster.&lt;/li&gt;
&lt;li&gt;The only app remaining on Windows was Plex, due to perceived difficult migrating all the metadata to Linux.&lt;/li&gt;
&lt;li&gt;All of the guides I found online had partial information, but not everything I needed to get a fully migrated Plex server up and running on my Linux machine!&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that I was migrating from a plain Windows install to a containerised Linux install, but for this guide, this info should not matter.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Official Guide
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;This has most of everything you need: &lt;a href="https://support.plex.tv/articles/201370363-move-an-install-to-another-system/" rel="noopener noreferrer"&gt;https://support.plex.tv/articles/201370363-move-an-install-to-another-system/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;However, do note that it's a guide explicitly for migrating to a new machine &lt;strong&gt;using the same operating system&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;There are two extra steps I will detail below, that should be carried out in the 'Copy Server Data From the Source System' section:

&lt;ul&gt;
&lt;li&gt;Updating paths from Windows to Linux in the Plex SQLite database.&lt;/li&gt;
&lt;li&gt;Copying preferences from Windows Registry to Linux XML file.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;After following the extra steps listed here, you should have an exact replica of the Plex installation you were migrating from!&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Update SQLite File Paths
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;First backup your SQLite database for Plex, located here:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;C:\Users\YOURUSER\AppData\Local\Plex Media Server\Plug-in Support\Databases\com.plexapp.plugins.library.db"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Enter the Plex SQLite shell
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="s2"&gt;"C:\Program Files\Plex\Plex Media Server\Plex SQLite.exe"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\Users\YOURUSER\AppData\Local\Plex Media Server\Plug-in Support\Databases\com.plexapp.plugins.library.db"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note that Plex uses a custom implementation of SQLite... standard browsers might corrupt your file, or just not work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;Replace Windows backslash with Linux forward slash
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;media_parts&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;, '&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;');
UPDATE section_locations SET root_path=REPLACE(root_path, '&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;');
UPDATE media_streams SET url=REPLACE(url, '&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s1"&gt;') WHERE url LIKE '&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//%&lt;/span&gt;&lt;span class="s1"&gt;';
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Replace library root paths
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="k"&gt;First&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt; &lt;span class="k"&gt;all&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;library&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="n"&gt;locations&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;root_path&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;section_locations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="k"&gt;For&lt;/span&gt; &lt;span class="k"&gt;each&lt;/span&gt; &lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;replace&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;correct&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;section_locations&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;root_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'D:/Path/To/Library1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/data/library1'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;section_locations&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;root_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'D:/Path/To/Library2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/data/library2'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Replace media part paths
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Inspect&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;media_parts&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nv"&gt;""&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="k"&gt;For&lt;/span&gt; &lt;span class="k"&gt;each&lt;/span&gt; &lt;span class="n"&gt;library&lt;/span&gt; &lt;span class="n"&gt;above&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="n"&gt;correctly&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="n"&gt;here&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;media_parts&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'D:/Path/To/Library1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/data/library1'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;media_parts&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'D:/Path/To/Library2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/data/library2'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Replace media stream paths
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Note&lt;/span&gt; &lt;span class="n"&gt;here&lt;/span&gt; &lt;span class="n"&gt;we&lt;/span&gt; &lt;span class="n"&gt;possibly&lt;/span&gt; &lt;span class="n"&gt;have&lt;/span&gt; &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;triple&lt;/span&gt; &lt;span class="n"&gt;slash&lt;/span&gt; &lt;span class="n"&gt;prefixes&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;must&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;replaced&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;media_streams&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;REPLACE&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="s1"&gt;'file://D:/Path/To/Library1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'file:///data/library1'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;media_streams&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;REPLACE&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="s1"&gt;'file:///D:/Path/To/Library1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'file:///data/library1'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;media_streams&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;REPLACE&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="s1"&gt;'file://D:/Path/To/Library2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'file:///data/library2'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;media_streams&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;REPLACE&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="s1"&gt;'file:///D:/Path/To/Library2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'file:///data/library2'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Replace metadata_item paths
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;metadata_items&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;guid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;guid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'file:///D:/Path/To/Library1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'file:///data/library1'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;metadata_items&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;guid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;guid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'file:///D:/Path/To/Library2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'file:///data/library2'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Final checks
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="k"&gt;Check&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;any&lt;/span&gt; &lt;span class="n"&gt;corruption&lt;/span&gt;
&lt;span class="n"&gt;PRAGMA&lt;/span&gt; &lt;span class="n"&gt;integrity_check&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;If&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;try&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;reindex&lt;/span&gt;
&lt;span class="k"&gt;REINDEX&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="k"&gt;Vacuum&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;
&lt;span class="k"&gt;VACUUM&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Exit&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Migrating Preferences
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Windows Registry
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Windows keeps it Plex preferences in the registry.&lt;/li&gt;
&lt;li&gt;To access the values, open tool &lt;strong&gt;regedit&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Navigate to path: &lt;code&gt;Computer\HKEY_CURRENT_USER\SOFTWARE\Plex, Inc.\Plex Media Server&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Inspect the values to migrate. Some important ones are:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PlexOnlineXXX&lt;/code&gt; values (for the PlexPass account)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;XXXMachineIdentifer&lt;/code&gt; values&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;customConnections&lt;/code&gt; if used, say for a reverse proxy config&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allowedNetworks&lt;/code&gt; if used&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Additional details can be found here: &lt;a href="https://support.plex.tv/articles/201105343-advanced-hidden-server-settings/" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="https://support.plex.tv/articles/201105343-advanced-hidden-server-settings/" rel="noopener noreferrer"&gt;https://support.plex.tv/articles/201105343-advanced-hidden-server-settings/&lt;/a&gt;
&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  Linux Preferences
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Linux places all of these values into a &lt;code&gt;Preferences.xml&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;First, start up and shut down your new Plex instance.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Preferences.xml&lt;/code&gt; file will be created in the 'Plex Media Server' root directory.&lt;/li&gt;
&lt;li&gt;Modify the preferences with the values from the Windows registry:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Preferences&lt;/span&gt;
  &lt;span class="na"&gt;allowedNetworks=&lt;/span&gt;&lt;span class="s"&gt;"127.0.0.1,192.168.90.11"&lt;/span&gt;
  &lt;span class="na"&gt;autoEmptyTrash=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;secureConnections=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;
  &lt;span class="na"&gt;sendCrashReports=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;
  &lt;span class="na"&gt;customConnections=&lt;/span&gt;&lt;span class="s"&gt;"https:/your.custom.domain:443"&lt;/span&gt;
  &lt;span class="na"&gt;AcceptedEULA=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt; 
  &lt;span class="na"&gt;AnonymousMachineIdentifier=&lt;/span&gt;&lt;span class="s"&gt;"xxx-xxx"&lt;/span&gt;
  &lt;span class="na"&gt;CertificateUUID=&lt;/span&gt;&lt;span class="s"&gt;"xxx-xxx"&lt;/span&gt;
  &lt;span class="na"&gt;CertificateVersion=&lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt;
  &lt;span class="na"&gt;CloudSyncNeedsUpdate=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;DatabaseCacheSize=&lt;/span&gt;&lt;span class="s"&gt;"4000"&lt;/span&gt;
  &lt;span class="na"&gt;DisableTLSv1_0=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;
  &lt;span class="na"&gt;DlnaEnabled=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;DvrIncrementalEpgLoader=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;EnableIPv6=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;FSEventLibraryPartialScanEnabled=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;FSEventLibraryUpdatesEnabled=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;FriendlyName=&lt;/span&gt;&lt;span class="s"&gt;"Name-Of-Your-Plex-Server"&lt;/span&gt;
  &lt;span class="na"&gt;GlobalMusicVideoPathMigrated=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;
  &lt;span class="na"&gt;LanNetworksBandwidth=&lt;/span&gt;&lt;span class="s"&gt;"192.168.90.11/24"&lt;/span&gt;
  &lt;span class="na"&gt;LanguageInCloud=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;
  &lt;span class="na"&gt;LastAutomaticMappedPort=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;LogVerbose=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;MachineIdentifier=&lt;/span&gt;&lt;span class="s"&gt;"xxx-xxx"&lt;/span&gt;
  &lt;span class="na"&gt;ManualPortMappingMode=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;
  &lt;span class="na"&gt;ManualPortMappingPort=&lt;/span&gt;&lt;span class="s"&gt;"32400"&lt;/span&gt;
  &lt;span class="na"&gt;MergedRecentlyAdded=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;MetricsEpoch=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;
  &lt;span class="na"&gt;MinutesAllowedPaused=&lt;/span&gt;&lt;span class="s"&gt;"30"&lt;/span&gt;
  &lt;span class="na"&gt;OldestPreviousVersion=&lt;/span&gt;&lt;span class="s"&gt;"legacy"&lt;/span&gt;
  &lt;span class="na"&gt;PlexOnlineHome=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;PlexOnlineMail=&lt;/span&gt;&lt;span class="s"&gt;"your@email.com"&lt;/span&gt;
  &lt;span class="na"&gt;PlexOnlineToken=&lt;/span&gt;&lt;span class="s"&gt;"token_from_previous_install"&lt;/span&gt;
  &lt;span class="na"&gt;PlexOnlineUsername=&lt;/span&gt;&lt;span class="s"&gt;"your_plex_username"&lt;/span&gt;
  &lt;span class="na"&gt;PreferredNetworkInterface=&lt;/span&gt;&lt;span class="s"&gt;"eth0"&lt;/span&gt;
  &lt;span class="na"&gt;ProcessedMachineIdentifier=&lt;/span&gt;&lt;span class="s"&gt;"xxx-xxx"&lt;/span&gt;
  &lt;span class="na"&gt;PubSubServer=&lt;/span&gt;&lt;span class="s"&gt;"xxx"&lt;/span&gt;
  &lt;span class="na"&gt;PubSubServerPing=&lt;/span&gt;&lt;span class="s"&gt;"xx"&lt;/span&gt;
  &lt;span class="na"&gt;PubSubServerRegion=&lt;/span&gt;&lt;span class="s"&gt;"lhr"&lt;/span&gt;
  &lt;span class="na"&gt;PublishServerOnPlexOnlineKey=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;
  &lt;span class="na"&gt;PushNotificationsEnabled=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;
  &lt;span class="na"&gt;RelayEnabled=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;ScannerLowPriority=&lt;/span&gt;&lt;span class="s"&gt;"1"&lt;/span&gt;
  &lt;span class="na"&gt;ScheduledLibraryUpdateInterval=&lt;/span&gt;&lt;span class="s"&gt;"86400"&lt;/span&gt;
  &lt;span class="na"&gt;ScheduledLibraryUpdatesEnabled=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;TranscodeCountLimit=&lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt;
  &lt;span class="na"&gt;TranscoderCanOnlyRemuxVideo=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;TranscoderH264BackgroundPreset=&lt;/span&gt;&lt;span class="s"&gt;"medium"&lt;/span&gt;
  &lt;span class="na"&gt;TranscoderQuality=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;TranscoderTempDirectory=&lt;/span&gt;&lt;span class="s"&gt;"/tmp/transcode"&lt;/span&gt;
  &lt;span class="na"&gt;TranscoderThrottleBuffer=&lt;/span&gt;&lt;span class="s"&gt;"1800"&lt;/span&gt;
  &lt;span class="na"&gt;WanPerUserStreamCount=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt;
  &lt;span class="na"&gt;WanTotalMaxUploadRate=&lt;/span&gt;&lt;span class="s"&gt;"20000"&lt;/span&gt;
  &lt;span class="na"&gt;WebHooksEnabled=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>tutorial</category>
      <category>server</category>
      <category>media</category>
      <category>plex</category>
    </item>
    <item>
      <title>Build a Simple Golang Webhook Service with Postgres Triggers and pg_notify</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Thu, 06 Feb 2025 17:45:08 +0000</pubDate>
      <link>https://dev.to/spwoodcock/building-a-simple-webhook-service-in-golang-using-postgres-triggers-and-notifications-3j72</link>
      <guid>https://dev.to/spwoodcock/building-a-simple-webhook-service-in-golang-using-postgres-triggers-and-notifications-3j72</guid>
      <description>&lt;h2&gt;
  
  
  Context For The Problem
&lt;/h2&gt;

&lt;p&gt;I work on a tool called the &lt;a href="https://github.com" rel="noopener noreferrer"&gt;Field Tasking Manager&lt;/a&gt; (Field-TM) at the Humanitarian OpenStreetMap Team.&lt;/p&gt;

&lt;p&gt;The goal is to coordinate field mapping activities, to both enrich OpenStreetMap data with field verified info, and collect field data for both development and humanitarian contexts.&lt;/p&gt;

&lt;p&gt;Underneath, we use two tools from &lt;a href="https://getodk.org" rel="noopener noreferrer"&gt;ODK&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/getodk/collect" rel="noopener noreferrer"&gt;Collect&lt;/a&gt; is used on mobile devices to send data to the &lt;a href="https://github.com/getodk/central" rel="noopener noreferrer"&gt;Central&lt;/a&gt; server.&lt;/p&gt;

&lt;p&gt;However, we need a way to be notified that a user has submitted new data from their phone, triggering an update from ODK Central --&amp;gt; Field-TM.&lt;/p&gt;

&lt;p&gt;There are three obvious approaches for this type of behaviour:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pull via polling&lt;/strong&gt;: polling of the ODK APIs at an interval (simple, requires no change to ODK, but resource inefficient).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push/pull via websocket&lt;/strong&gt;: bi-directional communication, but potentially complex to implement, and requires full development control of both services (requires significant changes to ODK).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push via webhook&lt;/strong&gt;: a change in ODK Central triggers a call to a remote API, including POST data (simple and efficient, requiring no persistent connection).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Using a webhook could potentially involve changes to the ODK codebase, but we can work around that - with the solution I will describe below!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There are of course other application communication methods that are out of scope of this article: event driven messaging, Redis pub/sub, gRPC, etc.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Postgres LISTEN / NOTIFY
&lt;/h2&gt;

&lt;p&gt;Having not previously used the LISTEN / NOTIFY notifications functionality in Postgres, I stumbled across this &lt;a href="https://brojonat.com/posts/go-postgres-listen-notify" rel="noopener noreferrer"&gt;excellent article&lt;/a&gt; by @brojonat, which in turn references &lt;a href="https://brandur.org/notifier" rel="noopener noreferrer"&gt;this article&lt;/a&gt; by @brandur.&lt;/p&gt;

&lt;p&gt;They describe a 'notifier pattern' for Postgres, where a connection is made to establish a 'listener'. Events in Postgres (such as a data insert) can be configured to trigger sending a notification to this 'listener', including the data that was inserted, or other data in the database.&lt;/p&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%2F21cvb4ahdveamzni01zf.jpg" 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%2F21cvb4ahdveamzni01zf.jpg" alt="notify-listen-postgres" width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;image credit to @brandur's post linked above.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This sounds perfect for a webhook service!&lt;/p&gt;

&lt;p&gt;It could be achieved like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a connection to Postgres, with a LISTEN for events.&lt;/li&gt;
&lt;li&gt;Create a TRIGGER in Postgres, to NOTIFY the listener and submit data.&lt;/li&gt;
&lt;li&gt;Parse the data in our service, then create a POST request with the data on a remote API endpoint (webhook).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ideally we could deploy this small service alongside ODK Central, scanning the Postgres database and triggering when new submission are made.&lt;/p&gt;

&lt;p&gt;This would mean no changes are required to the codebase of the application we develop the webhook for - making this a performant, but non-intrusive method of implementing via middleware.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Golang?
&lt;/h2&gt;

&lt;p&gt;I would say that I am far from proficient in Golang at this point, with Python being my preferred backend language.&lt;/p&gt;

&lt;p&gt;Python has the excellent &lt;code&gt;psycopg&lt;/code&gt; library for interfacing with Postgres, and has some great documentation for &lt;a href="https://www.psycopg.org/psycopg3/docs/advanced/async.html#asynchronous-notifications" rel="noopener noreferrer"&gt;how to use LISTEN / NOTIFY&lt;/a&gt; in Python.&lt;/p&gt;

&lt;p&gt;However, as a language, Golang has many advantages for this type of project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High performance / resource usage ratio, with a low memory footprint.&lt;/li&gt;
&lt;li&gt;Statically compiled binary that can be distributed anywhere. Simply download the binary / executable and run, without worrying about dependencies, versions, etc.&lt;/li&gt;
&lt;li&gt;Has a very comprehensive standard library, meaning it should require little maintenance going forward (minimal dependency upgrades).&lt;/li&gt;
&lt;li&gt;Reasonably simple to use. Of course, it has a few extra concepts then Python, such a pointers, composition, and goroutines, but not a huge lift to learn.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Alternative Approaches
&lt;/h2&gt;

&lt;p&gt;It should be noted there are alternative approaches to achieve the goals, which may or may not be better depending on your requirements.&lt;/p&gt;

&lt;p&gt;Three notable examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Instead of implementing a custom listener/notifier, it's possible to use existing solutions, such as this &lt;a href="https://github.com/geobabbler/pg_webhooks" rel="noopener noreferrer"&gt;Express app&lt;/a&gt; that be configured by JSON to listen for events and trigger webhooks. See related &lt;a href="https://blog.geomusings.com/2023/07/13/a-simple-webhook-interface-for-notify" rel="noopener noreferrer"&gt;blog&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Possibly you can call the webhook directly from a Postgres trigger using &lt;a href="https://github.com/pramsey/pgsql-http" rel="noopener noreferrer"&gt;pgsql-http&lt;/a&gt;, as long as the API has no authentication requirements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/Nextdoor/pg-bifrost" rel="noopener noreferrer"&gt;This&lt;/a&gt; very neat logical-replication based approach to do similar.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Implementation
&lt;/h2&gt;

&lt;p&gt;Right, now let's dig into the code.&lt;/p&gt;

&lt;p&gt;I won't replicate much here, as it's all available on the linked &lt;a href="https://github.com/hotosm/central-webhook/blob/main/db/connection.go" rel="noopener noreferrer"&gt;code repo&lt;/a&gt;, but will attempt to explain how the tool works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/hotosm/central-webhook/blob/main/db/connection.go" rel="noopener noreferrer"&gt;&lt;strong&gt;db/connection.go&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Here we use the github.com/jackc/pgx/v5/pgxpool package (the only external dependency used) as the Postgres database driver. The &lt;code&gt;lib/pq&lt;/code&gt; package might look more official at first glance, but it is community led and effectively in &lt;a href="https://github.com/lib/pq?tab=readme-ov-file#status" rel="noopener noreferrer"&gt;maintenance mode only&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/hotosm/central-webhook/blob/main/db/listener.go" rel="noopener noreferrer"&gt;&lt;strong&gt;db/listener.go&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/hotosm/central-webhook/blob/main/db/notifier.go" rel="noopener noreferrer"&gt;&lt;strong&gt;db/notifier.go&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is where we implement the LISTEN / NOTIFY functionality, as described in the previous section / blogs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/hotosm/central-webhook/blob/main/db/trigger.go" rel="noopener noreferrer"&gt;&lt;strong&gt;db/trigger.go&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Here we have a reasonably complex TRIGGER function, with CASE statements to handle three different scenarios.&lt;/li&gt;
&lt;li&gt;This is the core logic of &lt;strong&gt;what&lt;/strong&gt; event we want to trigger the webhook. Your implementation could be significantly simpler.&lt;/li&gt;
&lt;li&gt;We are using the ODK Central functionality called &lt;a href="https://docs.getodk.org/central-server-audits" rel="noopener noreferrer"&gt;audit logs&lt;/a&gt; here, essentially an event stream we can hook into and trigger our notifications from.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;new_audit_log&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;trigger&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
    &lt;span class="n"&gt;js&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;action_type&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;result_data&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="c1"&gt;-- Serialize the NEW row into JSONB&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;to_jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Add the DML action (INSERT/UPDATE)&lt;/span&gt;
    &lt;span class="n"&gt;js&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonb_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{dml_action}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TG_OP&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;-- Extract the action type from the NEW row&lt;/span&gt;
    &lt;span class="n"&gt;action_type&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;-- Handle different action types with a CASE statement&lt;/span&gt;
    &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;action_type&lt;/span&gt;
        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'entity.update.version'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;entity_defs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;
            &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;result_data&lt;/span&gt;
            &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;entity_defs&lt;/span&gt;
            &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;entity_defs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'entityDefId'&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="c1"&gt;-- Merge the entity details into the JSON data key&lt;/span&gt;
            &lt;span class="n"&gt;js&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonb_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{data}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;-- Notify the odk-events queue&lt;/span&gt;
            &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'odk-events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'submission.create'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'xml'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;submission_defs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;result_data&lt;/span&gt;
            &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;submission_defs&lt;/span&gt;
            &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;submission_defs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'submissionDefId'&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="c1"&gt;-- Merge the submission XML into the JSON data key&lt;/span&gt;
            &lt;span class="n"&gt;js&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonb_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{data}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;-- Notify the odk-events queue&lt;/span&gt;
            &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'odk-events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="s1"&gt;'submission.update'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
            &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'instanceId'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;submission_defs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"instanceId"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;result_data&lt;/span&gt;
            &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;submission_defs&lt;/span&gt;
            &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;submission_defs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'submissionDefId'&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="c1"&gt;-- Extract 'reviewState' from 'details' and set it in 'data'&lt;/span&gt;
            &lt;span class="n"&gt;js&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonb_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{data}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'reviewState'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'details'&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'reviewState'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;-- Remove 'reviewState' from 'details'&lt;/span&gt;
            &lt;span class="n"&gt;js&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonb_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{details}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'details'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'reviewState'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;-- Merge the instanceId into the existing 'details' key in JSON&lt;/span&gt;
            &lt;span class="n"&gt;js&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonb_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'{details}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'details'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;result_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="c1"&gt;-- Notify the odk-events queue&lt;/span&gt;
            &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'odk-events'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;js&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;ELSE&lt;/span&gt;
            &lt;span class="c1"&gt;-- Skip pg_notify for unsupported actions &amp;amp; insert as normal&lt;/span&gt;
            &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="s1"&gt;'plpgsql'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/hotosm/central-webhook/blob/main/parser/audit.go" rel="noopener noreferrer"&gt;&lt;strong&gt;parser/audit.go&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The code to parse the TRIGGER output into a Go struct, the marshal a JSON for sending to the webhook.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/hotosm/central-webhook/blob/main/webhook/request.go" rel="noopener noreferrer"&gt;&lt;strong&gt;webhook/request.go&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send a request with the given JSON payload, to the configured webhook URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/hotosm/central-webhook/blob/main/webhook/auth.go" rel="noopener noreferrer"&gt;&lt;strong&gt;webhook/auth.go&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handle authentication on the API by means of an &lt;code&gt;X-API-Key&lt;/code&gt; request header.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Outcome
&lt;/h2&gt;

&lt;p&gt;The final code repository can be found &lt;a href="https://github.com/hotosm/central-webhook" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The final binary is ~10MB in size, 15MB when distributed in a minimal container image, and only consumes ~5MB of memory at idle.&lt;/p&gt;

&lt;p&gt;Of course, this uses a connection to the Postgres database (that is cycled continually to remain alive), but overall I am extremely happy with this approach of a small lightweight service to run alongside ODK.&lt;/p&gt;

&lt;p&gt;Forum discussion around the approach can be found &lt;a href="https://forum.getodk.org/t/webhooks-in-odk-central/39917" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repo issue and linked PRs related to implementation with Field-TM can be found &lt;a href="https://github.com/hotosm/fmtm/issues/1841" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>go</category>
      <category>webhook</category>
      <category>microservices</category>
    </item>
    <item>
      <title>FastAPI, Pydantic, Psycopg3: The Ultimate Trio for Python Web APIs</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Thu, 24 Oct 2024 23:12:43 +0000</pubDate>
      <link>https://dev.to/spwoodcock/fastapi-pydantic-psycopg3-the-holy-trinity-for-python-web-apis-5b88</link>
      <guid>https://dev.to/spwoodcock/fastapi-pydantic-psycopg3-the-holy-trinity-for-python-web-apis-5b88</guid>
      <description>&lt;h2&gt;
  
  
  Part 1: Discussion
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Enter FastAPI
&lt;/h3&gt;

&lt;p&gt;First of all, take the title with a pinch of salt.&lt;/p&gt;

&lt;p&gt;If I was starting from scratch with Python web API development today, I would probably look more closely at &lt;a href="https://github.com/litestar-org/litestar" rel="noopener noreferrer"&gt;LiteStar&lt;/a&gt;, which seems to me to be a better architected and with a better project governance structure.&lt;/p&gt;

&lt;p&gt;But we have FastAPI and it's not going anywhere soon. I use it for a lot of personal and professional projects and still enjoy its simplicity.&lt;/p&gt;

&lt;p&gt;For a guide on FastAPI design patterns, look no further than &lt;a href="https://sqr-072.lsst.io" rel="noopener noreferrer"&gt;this page&lt;/a&gt;.&lt;/p&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%2Fv4dy3k98ksrmpau2aynp.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%2Fv4dy3k98ksrmpau2aynp.png" alt="fastapi" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieving Database Data
&lt;/h3&gt;

&lt;p&gt;Despite FastAPI being great at the actual 'API' part, there has been one persistent uncertainty for me: how to best access the database, particularly if we need to also handle geospatial data types.&lt;/p&gt;

&lt;p&gt;Let's review our options.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note 1: we are only interested in &lt;strong&gt;async&lt;/strong&gt; libraries here, as FastAPI is &lt;a href="https://asgi.readthedocs.io/en/latest" rel="noopener noreferrer"&gt;ASGI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Note 2: I will only discuss connecting to PostgreSQL, although parts of the discussion are still relevant to other databases.&lt;/p&gt;
&lt;/blockquote&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%2Ff0gqmgel40d3811wdhgs.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%2Ff0gqmgel40d3811wdhgs.png" alt="orm" width="536" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Simple To Code | Complex Design: ORMs
&lt;/h4&gt;

&lt;p&gt;Handles your database connection and parsing of data from your database table into Python objects.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html" rel="noopener noreferrer"&gt;SQLAlchemy2&lt;/a&gt;: the biggest contender in the Python ORM world. Personally I really dislike the syntax, but each to their own.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/tortoise/tortoise-orm" rel="noopener noreferrer"&gt;TortoiseORM&lt;/a&gt;: I personally really like this Django-inspired async ORM; it's clean and nice to use.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Alternative ORMs: there are many such as &lt;a href="https://github.com/coleifer/peewee" rel="noopener noreferrer"&gt;peewee&lt;/a&gt;, &lt;a href="https://github.com/ponyorm/pony" rel="noopener noreferrer"&gt;PonyORM&lt;/a&gt;, etc.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  The Middle Ground: Query Builders
&lt;/h4&gt;

&lt;p&gt;No database connection. Simply output raw SQL from a Python-based query and pass it to the database driver.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.sqlalchemy.org/en/20/core" rel="noopener noreferrer"&gt;SQLAlchemy Core&lt;/a&gt;: the core SQL query builder, without the mapping to objects part. There is also a higher level ORM built on this called &lt;a href="https://github.com/encode/databases" rel="noopener noreferrer"&gt;databases&lt;/a&gt; that looks very nice. I do wonder how actively developed the project is however.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/kayak/pypika" rel="noopener noreferrer"&gt;PyPika&lt;/a&gt;: I don't know much about this one.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Simple Design: Database Drivers
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/MagicStack/asyncpg" rel="noopener noreferrer"&gt;asyncpg&lt;/a&gt;: this was the gold standard async database driver for Postgres, being one of the first to market and most performant. While all other drivers use the C library &lt;code&gt;libpq&lt;/code&gt; to interface with Postgres, MagicStack opted to rewrite their own custom implementation and also deviate from Python &lt;a href="https://peps.python.org/pep-0249" rel="noopener noreferrer"&gt;DBAPI&lt;/a&gt; spec. If performance is your main criteria here, then &lt;code&gt;asyncpg&lt;/code&gt; is probably the best option.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/psycopg/psycopg" rel="noopener noreferrer"&gt;psycopg3&lt;/a&gt;: well &lt;code&gt;psycopg2&lt;/code&gt; was clearly the king of the &lt;strong&gt;synchronous&lt;/strong&gt; database driver world for Python/Postgres. &lt;code&gt;psycopg3&lt;/code&gt; (rebranded to simply &lt;code&gt;psycopg&lt;/code&gt;) is the next, fully async, iteration of this library. This library has really come into it's own in recent years &amp;amp; I wish to discuss it further. See this interesting &lt;a href="https://www.varrazzo.com/blog/2020/05/19/a-trip-into-optimisation" rel="noopener noreferrer"&gt;blog&lt;/a&gt; from the author about the early days of psycopg3.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that there is clearly a broader, more conceptual, discussion to be had here around ORMs vs query builders vs raw SQL. I won't cover that here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Duplicated Models
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/pydantic/pydantic" rel="noopener noreferrer"&gt;Pydantic&lt;/a&gt; is bundled with FastAPI and is excellent for modelling, validating, and serialising API responses.&lt;/p&gt;

&lt;p&gt;If we decide to use an ORM to retrieve data from our database, isn't it a bit inefficient keeping two sets of database models in sync? (one for the ORM, another for Pydantic)?&lt;/p&gt;

&lt;p&gt;Wouldn't it be great if we could just use Pydantic to model the database?&lt;/p&gt;

&lt;p&gt;This is exactly the problem the creator of FastAPI tried to solve with the library &lt;a href="https://github.com/fastapi/sqlmodel" rel="noopener noreferrer"&gt;SQLModel&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;While this could very well be a great solution to the problem, I have a few concerns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Will this project suffer from the single-maintainer syndrome like FastAPI?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It's still a reasonably young project and concept, where documentation isn't fantastic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It's intrinsically tied up with Pydantic and SQLAlchemy, meaning migration away would be extremely difficult.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For more complex queries, dropping down to SQLAlchemy underneath may be required.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Back To Basics
&lt;/h3&gt;

&lt;p&gt;So many options! &lt;a href="https://en.wikipedia.org/wiki/Analysis_paralysis" rel="noopener noreferrer"&gt;Analysis paralysis&lt;/a&gt;.&lt;/p&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%2Fdnwi9nlwrumfl06ib9no.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%2Fdnwi9nlwrumfl06ib9no.png" alt="analysis-paralysis" width="800" height="593"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When there is uncertainty I would use the following precept: keep it simple.&lt;/p&gt;

&lt;p&gt;SQL was invented 50yrs ago and is still a key skill for any developer to learn. It's syntax is consistently easy to grasp and uncomplicated to write for most use cases (for the die-hard ORM users out there, give it a try, you might be surprised).&lt;/p&gt;

&lt;p&gt;Hell, we can even use open-source LLMs these days to generate (mostly working) SQL queries and save you the typing.&lt;/p&gt;

&lt;p&gt;While ORMs and query builders may come and go, database drivers are likely more consistent. The original &lt;code&gt;psycopg2&lt;/code&gt; library was written nearly 20yrs ago now and is still actively used in production globally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Psycopg with Pydantic Models
&lt;/h3&gt;

&lt;p&gt;As discussed, while &lt;code&gt;psycopg&lt;/code&gt; may not be as performant as &lt;code&gt;asyncpg&lt;/code&gt; (the real world implications of this theoretical performance is debatable though), &lt;code&gt;psycopg&lt;/code&gt; focuses on ease of use and a familiar API.&lt;/p&gt;

&lt;p&gt;The killer feature for me is &lt;a href="https://www.psycopg.org/psycopg3/docs/advanced/rows.html" rel="noopener noreferrer"&gt;Row Factories&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This functionality allows you to map returned database data to any Python object, including standard lib &lt;a href="https://docs.python.org/3/library/dataclasses.html" rel="noopener noreferrer"&gt;dataclasses&lt;/a&gt;, models from the great &lt;a href="https://github.com/python-attrs/attrs" rel="noopener noreferrer"&gt;attrs&lt;/a&gt; library, and yes, &lt;a href="https://www.psycopg.org/psycopg3/docs/advanced/typing.html#example-returning-records-as-pydantic-models" rel="noopener noreferrer"&gt;Pydantic models&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;For me, this is the best compromise of approaches: the ultimate flexibility of raw SQL, with the validation / type safety capabilities of Pydantic to model the database. Psycopg also handles things like variable input sanitation to avoid SQL injection.&lt;/p&gt;

&lt;p&gt;It should be noted that &lt;code&gt;asyncpg&lt;/code&gt; can also handle mapping to Pydantic models, but as more of a workaround than a built-in feature. See &lt;a href="https://github.com/pydantic/pydantic/issues/9406#issuecomment-2104224328" rel="noopener noreferrer"&gt;this issue thread&lt;/a&gt; for details. I also don't know if this approach plays nicely with other modelling libraries.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As I mentioned above, I typically work with geospatial data: an area often neglected by ORMs and query builders. Dropping to the raw SQL gives me the ability to parse and unparse geospatial data as I need to more acceptable types in pure Python. See my &lt;a href="https://dev.to/spwoodcock/leveraging-postgis-to-write-and-read-flatgeobuf-files-1bp2"&gt;related article&lt;/a&gt; on this topic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Part 2: Example Usage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create A Database Table
&lt;/h3&gt;

&lt;p&gt;Here we create a simple database table called &lt;code&gt;user&lt;/code&gt; in raw SQL.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I would also consider handling database creation and migrations using SQL only, but this is a topic for another article.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;init_db.sql&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userrole&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;ENUM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'READ_ONLY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'STANDARD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'ADMIN'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&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;id&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;role&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userrole&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'STANDARD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;profile_img&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email_address&lt;/span&gt; &lt;span class="nb"&gt;character&lt;/span&gt; &lt;span class="nb"&gt;varying&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_email_verified&lt;/span&gt; &lt;span class="nb"&gt;boolean&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;registered_at&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&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;h3&gt;
  
  
  Model Your Database With Pydantic
&lt;/h3&gt;

&lt;p&gt;Here we create a model called DbUser:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db_models.py&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;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic.functional_validators&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;field_validator&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;geojson_pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Feature&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRole&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="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Types of user, mapped to database enum userrole.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;READ_ONLY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;READ_ONLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;STANDARD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STANDARD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;ADMIN&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DbUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Table users.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;UserRole&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;UserRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;STANDARD&lt;/span&gt;
    &lt;span class="n"&gt;profile_img&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&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;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;email_address&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&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;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;is_email_verified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;registered_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# This is a geospatial type I will handle in the SQL
&lt;/span&gt;    &lt;span class="n"&gt;favourite_place&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&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="c1"&gt;# DB computed fields (handled in the SQL)
&lt;/span&gt;    &lt;span class="n"&gt;total_users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&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;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="c1"&gt;# This example isn't very realistic, but you get the idea
&lt;/span&gt;    &lt;span class="nd"&gt;@field_validator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_email_verified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;before&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@classmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;i_want_my_ints_as_bools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&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;bool&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 of a validator to convert data type.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we get the type safety and validation of Pydantic.&lt;/p&gt;

&lt;p&gt;We can add any form of validation or data transformation to this model for when the data is extracted from the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up Psycopg With FastAPI
&lt;/h3&gt;

&lt;p&gt;We use psycopg_pool to create a &lt;strong&gt;pooled&lt;/strong&gt; database connection:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db.py&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;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cast&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg_pool&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncConnectionPool&lt;/span&gt;

&lt;span class="c1"&gt;# You should be using environment variables in a settings file here
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_db_connection_pool&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;AsyncConnectionPool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get the connection pool for psycopg.

    NOTE the pool connection is opened in the FastAPI server startup (lifespan).

    Also note this is also a sync `def`, as it only returns a context manager.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AsyncConnectionPool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;conninfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DB_URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unicode_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;db_conn&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get a connection from the psycopg pool.

    Info on connections vs cursors:
    https://www.psycopg.org/psycopg3/docs/advanced/async.html

    Here we are getting a connection from the pool, which will be returned
    after the session ends / endpoint finishes processing.

    In summary:
    - Connection is created on endpoint call.
    - Cursors are used to execute commands throughout endpoint.
      Note it is possible to create multiple cursors from the connection,
      but all will be executed in the same db &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;transaction&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.
    - Connection is closed on endpoint finish.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;db_pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AsyncConnectionPool&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db_pool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;db_pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next we open the connection pool in the FastAPI lifespan event:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;main.py&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;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncIterator&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asynccontextmanager&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_db_connection_pool&lt;/span&gt;

&lt;span class="nd"&gt;@asynccontextmanager&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lifespan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FastAPI&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;AsyncIterator&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;FastAPI startup/shutdown event.&lt;/span&gt;&lt;span class="sh"&gt;"""&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;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Starting up FastAPI server.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Create a pooled db connection and make available in lifespan state
&lt;/span&gt;    &lt;span class="c1"&gt;# https://asgi.readthedocs.io/en/latest/specs/lifespan.html#lifespan-state
&lt;/span&gt;    &lt;span class="c1"&gt;# NOTE to use within a request (this is wrapped in database.py already):
&lt;/span&gt;    &lt;span class="c1"&gt;# from typing import cast
&lt;/span&gt;    &lt;span class="c1"&gt;# db_pool = cast(AsyncConnectionPool, request.state.db_pool)
&lt;/span&gt;    &lt;span class="c1"&gt;# async with db_pool.connection() as conn:
&lt;/span&gt;    &lt;span class="n"&gt;db_pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_db_connection_pool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db_pool&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="k"&gt;yield&lt;/span&gt;

    &lt;span class="c1"&gt;# Shutdown events
&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;Shutting down FastAPI server.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Here we make sure to close the connection pool
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db_pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when you FastAPI app starts, you should have an open connection pool, ready to take connection from inside endpoints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Helper Methods For The Pydantic Model
&lt;/h3&gt;

&lt;p&gt;It would be useful to add a few methods to the Pydantic model for common functionality: getting one user, all users, creating a user, updating a user, deleting a user.&lt;/p&gt;

&lt;p&gt;But first we should create some Pydantic models for input validation (to create a new user) and output serialisation (your JSON response via the API).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;user_schemas.py&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;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic.functional_validators&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;field_validator&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;geojson_pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FeatureCollection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MultiPolygon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Polygon&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.db_models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DbUser&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbUser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;User details for insert into DB.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="c1"&gt;# Exclude fields not required for input
&lt;/span&gt;    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&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="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exclude&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="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;favourite_place&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Feature&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nd"&gt;@field_validator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;favourite_place&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;before&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@classmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_input_geojson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FeatureCollection&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Feature&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;MultiPolygon&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Polygon&lt;/span&gt;&lt;span class="p"&gt;,&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="n"&gt;Polygon&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Parse any format geojson into a single Polygon.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;is&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;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="c1"&gt;# NOTE I don't include this helper function for brevity
&lt;/span&gt;        &lt;span class="n"&gt;featcol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalise_to_single_geom_featcol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&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;featcol&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;features&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="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;geometry&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;UserOut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbUser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;User details for insert into DB.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="c1"&gt;# Ensure it's parsed as a Polygon geojson from db object
&lt;/span&gt;    &lt;span class="n"&gt;favourite_place&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Polygon&lt;/span&gt;

    &lt;span class="c1"&gt;# More logic to append computed values
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we can define our helper methods: &lt;code&gt;one&lt;/code&gt;, &lt;code&gt;all&lt;/code&gt;, &lt;code&gt;create&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db_models.py&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="p"&gt;...&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="n"&gt;imports&lt;/span&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;Self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg.rows&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;class_row&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.user_schemas&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UserIn&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DbUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Table users.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;

    &lt;span class="nd"&gt;@classmethod&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&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;Self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get a user by ID.

        NOTE how the favourite_place field is converted in the db to geojson.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;class_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                SELECT
                    u.*,
                    ST_AsGeoJSON(favourite_place)::jsonb AS favourite_place,
                    (SELECT COUNT(*) FROM users) AS total_users
                FROM users u
                WHERE
                    u.id = %(user_id)s
                GROUP BY u.id;
            &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;sql&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;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;db_project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&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;db_project&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;KeyError&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;User (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_identifier&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;) not found.&lt;/span&gt;&lt;span class="sh"&gt;"&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;db_project&lt;/span&gt;

    &lt;span class="nd"&gt;@classmethod&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&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="nb"&gt;list&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetch all users.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;class_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
                SELECT
                    *,
                    ST_AsGeoJSON(favourite_place)::jsonb
                FROM users
                OFFSET %(offset)s
                LIMIT %(limit)s;
                &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;offset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nd"&gt;@classmethod&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;user_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UserIn&lt;/span&gt;&lt;span class="p"&gt;,&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="n"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create a new user.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

        &lt;span class="c1"&gt;# Omit defaults and empty values from the model
&lt;/span&gt;        &lt;span class="n"&gt;model_dump&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_in&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exclude_none&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;exclude_default&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;columns&lt;/span&gt; &lt;span class="o"&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;value_placeholders&lt;/span&gt; &lt;span class="o"&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="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="s"&gt;%(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)s&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;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

        &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&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;
            INSERT INTO users
                (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)
            VALUES
                (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;value_placeholders&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)
            RETURNING *;
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;


        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;class_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;new_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;new_user&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&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;Unknown SQL error for data: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model_dump&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;Failed user creation: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;model_dump&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;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;msg&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;new_user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;routes.py&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;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;psycopg&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.main&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;db_conn&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DbUser&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.user_schemas&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UserIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UserOut&lt;/span&gt;

&lt;span class="nd"&gt;@app.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;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UserOut&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;user_info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UserIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_conn&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;Create a new user.

    Here the input is parsed and validated by UserIn
    then the output is parsed and validated by UserOut
    returning the user json data.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;new_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;DbUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_info&lt;/span&gt;&lt;span class="p"&gt;)&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;new_user&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;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User creation failed.&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;new_user&lt;/span&gt;

    &lt;span class="c1"&gt;# NOTE within an endpoint we can also use
&lt;/span&gt;    &lt;span class="c1"&gt;# DbUser.one(db, user_id) and DbUser.all(db)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the approach I have started to use in a project I maintain, the &lt;a href="https://www.hotosm.org/updates/field-mapping-tasking-manager-fmtm" rel="noopener noreferrer"&gt;FMTM&lt;/a&gt;, a tool to collect field data for communities around the world.&lt;/p&gt;

&lt;p&gt;See the full codebase &lt;a href="https://github.com/hotosm/fmtm" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;br&gt;
And ⭐ if you found this useful!&lt;/p&gt;

&lt;p&gt;That's all for now! I hope this helps someone out there 🚀&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>postgres</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Contributing to ODK: the go-to mobile form data collection tool</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Thu, 13 Jun 2024 21:36:16 +0000</pubDate>
      <link>https://dev.to/spwoodcock/contributing-to-odk-the-go-to-mobile-form-data-collection-tool-3mco</link>
      <guid>https://dev.to/spwoodcock/contributing-to-odk-the-go-to-mobile-form-data-collection-tool-3mco</guid>
      <description>&lt;p&gt;This is a post with very little code, but instead a higher level retrospective on contribution to a mature open-source project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intro to ODK
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://getodk.org" rel="noopener noreferrer"&gt;ODK&lt;/a&gt; is a collection of open-source tools that are used to collect data via forms designed for a mobile phone.&lt;/p&gt;

&lt;p&gt;It is at the forefront of tools available for this purpose, commonly used in sectors such as public health, global development, crisis response, environmental research, and more.&lt;/p&gt;

&lt;p&gt;The key tools to be aware of are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ODK Collect&lt;/strong&gt;: the Android application written in Java/Kotlin, used to collect data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ODK Central&lt;/strong&gt;: the Node.js web server written in JavaScript, used to receive the data from mobile devices. There is a companion web frontend written in Vue.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Additional requirements from field mappers
&lt;/h2&gt;

&lt;p&gt;At the Humanitarian OpenStreetMap team (HOT), we use ODK to underpin one of our core tools, the Field Mapping Tasking Manager (&lt;a href="https://github.com/hotosm/fmtm" rel="noopener noreferrer"&gt;FMTM&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Mappers in the field must use ODK Collect to gather information about features on the ground (buildings, roads, etc).&lt;/p&gt;

&lt;p&gt;However, there are two major features that would significantly improve the workflow for mappers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;To have an easy way to load base satellite imagery in the background of ODK Collect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To open ODK Collect from the click of a button in FMTM, with information pre-filled, such as a selected building.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Luckily, the ODK community and developers, are both very welcoming, and also very receptive to feedback and new ideas.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing easier MBTile usage
&lt;/h3&gt;

&lt;p&gt;ODK has supported loading basemaps in MBTile format for a while now, however adding them to a project requires connecting a computer and manually copying the files over.&lt;/p&gt;

&lt;p&gt;After fruitful &lt;a href="https://forum.getodk.org/t/provide-a-way-to-get-mbtiles-to-collect-without-having-to-connect-to-a-computer/42206" rel="noopener noreferrer"&gt;discussion&lt;/a&gt; around this task on the ODK forums, a plan was made by the ODK devs to improve the implementation.&lt;/p&gt;

&lt;p&gt;HOT works closely with &lt;a href="https://naxa.com.np" rel="noopener noreferrer"&gt;NAXA&lt;/a&gt; for development on FMTM, and NAXA's current Chief Operating Officer is incidentally one of their most senior Android devs!&lt;/p&gt;

&lt;p&gt;Nishon managed to put in a big shift to implement most of the requirements for loading the MBTiles file directly from a device &lt;a href="https://github.com/getodk/collect/pull/5917" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Unfortunately due to lack of capacity from our side, the final touches to the PR were not able to be made 😅 Grzegorz from the ODK team picked up the feature and finalised it for their most recent beta release (2024.2.0-beta.2).&lt;/p&gt;

&lt;p&gt;However, overall it was a very successful collaboration &amp;amp; we are extremely grateful to the ODK team for being so responsive and receptive to contribution. &lt;/p&gt;

&lt;h3&gt;
  
  
  Loading ODK Collect by intent
&lt;/h3&gt;

&lt;p&gt;More recently we have been focusing on our second requirement: loading ODK Collect from an external app, with data pre-filled.&lt;/p&gt;

&lt;p&gt;We started &lt;a href="https://forum.getodk.org/t/launch-odkcollect-from-a-browser-with-feature-pre-selected/43190/8" rel="noopener noreferrer"&gt;discussion&lt;/a&gt; on this a while ago, gathering community input and requirements.&lt;/p&gt;

&lt;p&gt;Ping, an very experienced dev who used to be part of ODK, helped us to create a &lt;a href="https://github.com/hotosm/odkcollect/pull/2" rel="noopener noreferrer"&gt;proof of concept&lt;/a&gt;. After further bug fixing by Samir at NAXA, we finally have a built dev APK with all of our requirements. &lt;/p&gt;

&lt;p&gt;Obviously this proof of concept would not pass ODK's very strict code quality requirements, including extensive testing and covering a well researched combination of user requirements.&lt;/p&gt;

&lt;p&gt;However, we plan to continue the discussion and development (primarily by Samir) for hopefully another successful collaboration with the ODK team.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other contributions
&lt;/h3&gt;

&lt;p&gt;During this process I have also been providing additional contributions to documentation and build configs (as my experience does not lie in Android development), to help build goodwill and aid the long term success of ODK.&lt;/p&gt;

&lt;p&gt;At the organisation level, and individual level, we are very keen to see the suite of ODK tools grow and the team to continue the exemplary job they are doing managing an open-source project!&lt;/p&gt;

&lt;p&gt;Some minor contributions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Helping debug the ODK Central deployment on ARM-based systems.&lt;/li&gt;
&lt;li&gt;Upgrading the Python version for pyxform-http.&lt;/li&gt;
&lt;li&gt;Tweaking the containerised deployment of Central.&lt;/li&gt;
&lt;li&gt;Proof reading and adding content to docs.&lt;/li&gt;
&lt;li&gt;Testing all the tools in various scenarios.&lt;/li&gt;
&lt;li&gt;Early adoption of new features and providing feedback.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Contributing to an established application with many complex user requirements is no easy task.&lt;/p&gt;

&lt;p&gt;Thankfully the developers at ODK are extremely professional about this process, and while expect a very thorough and professional contribution, they are open to assist and provide guidance throughout the entire process.&lt;/p&gt;

&lt;p&gt;It may take some extra time to gather user requirements, provide comprehensive tests, and review the code more thoroughly than normally accustomed, however in the end it has been very rewarding. ODK is, and remains to be, the most reliable and complete suite of tools for form-based data collection, and hopefully with the support of the open-source community will continue to be long into the future!&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>data</category>
      <category>community</category>
    </item>
    <item>
      <title>Using PostGIS to Read and Write FlatGeobuf Files (With Examples)</title>
      <dc:creator>Sam</dc:creator>
      <pubDate>Thu, 13 Jun 2024 13:41:29 +0000</pubDate>
      <link>https://dev.to/spwoodcock/leveraging-postgis-to-write-and-read-flatgeobuf-files-1bp2</link>
      <guid>https://dev.to/spwoodcock/leveraging-postgis-to-write-and-read-flatgeobuf-files-1bp2</guid>
      <description>&lt;p&gt;Post adapted from &lt;a href="https://www.openstreetmap.org/user/spwoodcock/diary/402948" rel="noopener noreferrer"&gt;https://www.openstreetmap.org/user/spwoodcock/diary/402948&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Flatgeobuf in Python
&lt;/h2&gt;

&lt;p&gt;&lt;a href="http://flatgeobuf.org" rel="noopener noreferrer"&gt;Flatgeobuf&lt;/a&gt; is an excellent replacement for &lt;a href="http://switchfromshapefile.org" rel="noopener noreferrer"&gt;shapefile&lt;/a&gt;, particularly for geospatial data on the web.&lt;/p&gt;

&lt;p&gt;With web/javascript being the main target, currently there is no official implementation in Python to read/write data.&lt;/p&gt;

&lt;p&gt;Instead devs should most likely use the GDAL Python bindings.&lt;/p&gt;

&lt;h2&gt;
  
  
  To GDAL or not to GDAL
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://gdal.org/index.html" rel="noopener noreferrer"&gt;GDAL&lt;/a&gt; is an incredible geospatial library and underpins so much of what we do, including our databases (PostGIS).&lt;/p&gt;

&lt;p&gt;However, sometimes it might be a bit heavyweight for what we are trying to achieve.&lt;/p&gt;

&lt;p&gt;Installing it as a base system dependency inevitably installs &lt;strong&gt;everything&lt;/strong&gt; - there are no options.&lt;/p&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%2F0tj5qvvuw4jutw5wikuk.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%2F0tj5qvvuw4jutw5wikuk.png" alt="GDAL Install" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Install size is especially important when building container images, that we want to be as small as possible for distribution.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  GDAL in PostGIS
&lt;/h2&gt;

&lt;p&gt;PostGIS uses GDAL for most of it's geospatial processing, including reading and writing various geospatial file formats.&lt;/p&gt;

&lt;p&gt;When developing a web application in Python, including a database, there is a good chance you are using Postgis already as part of your software stack.&lt;/p&gt;

&lt;p&gt;So today I thought: why not just use the geospatial processing built into PostGIS for reading and writing flatgeobuf data?&lt;/p&gt;

&lt;p&gt;This would save having to install GDAL alongside my Python API and reduce container image size significantly.&lt;/p&gt;

&lt;p&gt;The solution wasn't super simple, but works quite nicely for my use case!&lt;/p&gt;

&lt;h2&gt;
  
  
  Database Access
&lt;/h2&gt;

&lt;p&gt;First we need a way to access the database.&lt;/p&gt;

&lt;p&gt;An example using SQLAlchemy could 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;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.engine&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_engine&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DeclarativeBase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Union&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="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get engine from existing Session, or connection string.
    If `db` is a connection string, a new engine is generated.
    &lt;/span&gt;&lt;span class="sh"&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;db&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_bind&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;elif&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;db&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&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;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The `db` variable is not a valid string or Session&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;msg&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;This example allows for an existing connection to be re-used from an endpoint, for example a FastAPI dependency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The nitty-gritty SQL
&lt;/h2&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;geojson&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FeatureCollection&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;geojson_to_flatgeobuf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&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;geojson&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FeatureCollection&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;From a given FeatureCollection, return a memory flatgeobuf obj.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&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;
        DROP TABLE IF EXISTS public.temp_features CASCADE;
        CREATE TABLE IF NOT EXISTS public.temp_features(
            id serial PRIMARY KEY,
            geom geometry
        );
        WITH data AS (SELECT &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;geojson&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;::json AS fc)
        INSERT INTO public.temp_features (geom)
        SELECT
            ST_AsText(ST_GeomFromGeoJSON(feat-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;geometry&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)) AS geom
        FROM (
            SELECT json_array_elements(fc-&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;features&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;) AS feat
            FROM data
        ) AS f;
        WITH thegeom AS
        (SELECT * FROM public.temp_features)
        SELECT ST_AsFlatGeobuf(thegeom.*)
        FROM thegeom;
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Run the SQL
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;# Get a memoryview object, then extract to Bytes
&lt;/span&gt;    &lt;span class="n"&gt;flatgeobuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;tobytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# Cleanup table
&lt;/span&gt;    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DROP TABLE IF EXISTS public.temp_features CASCADE;&lt;/span&gt;&lt;span class="sh"&gt;"&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;flatgeobuf&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function requires a FeatureCollection geojson.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Now I'm sure this is a much more efficient way to write this by nesting SQL SELECTs, but I was too lazy to debug and I find this approach quite readable, albeit slightly less efficient.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Using the code
&lt;/h2&gt;

&lt;p&gt;An example of using in FastAPI:&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;geojson&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.db.postgis_utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;geojson_to_flatgeobuf&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BytesIO&lt;/span&gt;

&lt;span class="nd"&gt;@router.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;/something&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;something&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;upload_geojson&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UploadFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
    &lt;span class="n"&gt;db&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="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;json_obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;upload_geojson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;parsed_geojson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;geojson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;flatgeobuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;geojson_to_flatgeobuf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parsed_geojson&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;do&lt;/span&gt; &lt;span class="n"&gt;something&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;flatgeobuf&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;There is one glaringly obvious limitation of this approach: if reading the FlatGeobuf is implemented in the same way then we lose the benefit of it's 'cloud native' encoding.&lt;/p&gt;

&lt;p&gt;Reading requires downloading the entire file, passing to PostGIS, and returning a GeoJSON.&lt;/p&gt;

&lt;p&gt;However, that was not the intended purpose of this workaround.&lt;/p&gt;

&lt;p&gt;FlatGeobuf is primarily a format meant for &lt;strong&gt;browser consumption&lt;/strong&gt;. With excellent support via the &lt;a href="https://www.npmjs.com/package/flatgeobuf" rel="noopener noreferrer"&gt;npm package&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So while the backend API can write data to FlatGeobuf without requiring dependencies, the frontend can then read the data if it's hosted somewhere online (i.e. an S3 bucket).&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading The Data Again in Python
&lt;/h2&gt;

&lt;p&gt;In some cases you may wish to access &lt;strong&gt;all&lt;/strong&gt; of the data, say to convert to a different format.&lt;/p&gt;

&lt;p&gt;This is also possible directly in the database.&lt;br&gt;
I ended up writing the reverse query flatgeobuf --&amp;gt; geojson:&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flatgeobuf_to_geojson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;db&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;flatgeobuf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&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="n"&gt;geojson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FeatureCollection&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Converts FlatGeobuf data to GeoJSON.

    Args:
        db (Session): SQLAlchemy db session.
        flatgeobuf (bytes): FlatGeobuf data in bytes format.

    Returns:
        geojson.FeatureCollection: A FeatureCollection object.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        DROP TABLE IF EXISTS public.temp_fgb CASCADE;

        SELECT ST_FromFlatGeobufToTable(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;public&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="s"&gt;temp_fgb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, :fgb_bytes);

        SELECT jsonb_build_object(
            &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="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FeatureCollection&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="s"&gt;features&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, jsonb_agg(feature)
        ) AS feature_collection
        FROM (
            SELECT jsonb_build_object(
                &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="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Feature&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="s"&gt;geometry&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, ST_AsGeoJSON(fgb_data.geom)::jsonb,
                &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, fgb_data.properties::jsonb
            ) AS feature
            FROM (
            SELECT *,
                NULL as properties
                FROM ST_FromFlatGeobuf(null::temp_fgb, :fgb_bytes)
            ) AS fgb_data
        ) AS features;
    &lt;/span&gt;&lt;span class="sh"&gt;"""&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="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&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;fgb_bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flatgeobuf&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;feature_collection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ProgrammingError&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;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="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&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;Attempted flatgeobuf --&amp;gt; geojson conversion, but duplicate column found&lt;/span&gt;&lt;span class="sh"&gt;"&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;None&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;feature_collection&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;geojson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature_collection&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;There are two steps required.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First a table must be created with fields representing the field types in the flatgeobuf.&lt;/li&gt;
&lt;li&gt;Then the data is extracted from the file, using the table type as reference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This wasn’t very intuitive to me &amp;amp; the PostGIS docs are really lacking here, so I hope this helps someone!&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>python</category>
      <category>sql</category>
      <category>geospatial</category>
      <category>flatgeobuf</category>
    </item>
  </channel>
</rss>
