<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title type="text">Peter Hoffmann</title>
  <id>https://peter-hoffmann.com/feed/atom.xml</id>
  <updated>2026-02-13T00:00:00Z</updated>
  <link href="https://peter-hoffmann.com" />
  <link href="https://peter-hoffmann.com/feed/atom.xml" rel="self" />
  <subtitle type="text">Atom Feed for peter-hoffmann.com</subtitle>
  <generator>Werkzeug</generator>
  <entry xml:base="https://peter-hoffmann.com/feed/atom.xml">
    <title type="text">Garmin inReach Mini 2 Leaflet checkin map</title>
    <id>https://peter-hoffmann.com/2026/garmin-inreach-mini-2-leaflet-checkin-map.html</id>
    <updated>2026-02-13T00:00:00Z</updated>
    <published>2026-02-13T00:00:00Z</published>
    <link href="/2026/garmin-inreach-mini-2-leaflet-checkin-map.html" />
    <author>
      <name>Peter Hoffmann</name>
      <uri>https://peter-hoffmann.com</uri>
    </author>
    <content type="html">&lt;p&gt;We will be trekking the eastern part of the Great Himalaya Trail in Nepal in
March/April. Details on the route and our plans can be found at
&lt;a href=&#34;https://greathimalayatrail.de&#34;&gt;https://greathimalayatrail.de&lt;/a&gt;. Our intent is to keep friends and family
updated on our progress. Given that we&#39;ll be hiking in quite remote areas, a
satellite phone/pager will be our sole means of communication.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/static/2026/garmin-inreach-mini.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;After the Garmin inReach Mini 3 was released recently, the Inreach Mini 2 was on
heavy sale. The inReach Mini 2 has all the features I need: satellite messaging,
check-ins, offline mode with navigation, and track recording.&lt;/p&gt;
&lt;h1&gt;Plans&lt;/h1&gt;
&lt;p&gt;I&#39;m on the Garmin Essential plan for 18 euros per month. It includes 50 free
text messages or weather requests each month, plus unlimited check-in messages.
The smaller Enabled plan (10 Euros) is missing the unlimited checkins, while the
the Standard plan (34 Euros) gives you 150 free messages and unlimited live tracking.
More details are on the &lt;a href=&#34;https://www.garmin.com/de-DE/p/837461/pn/010-04015-SU/&#34;&gt;Garmin page&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;Messaging&lt;/h1&gt;
&lt;p&gt;There are three different type of messages that you can send:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Check-In Messages:&lt;/strong&gt; There are three preset messages. You can configure the
recipients at &lt;a href=&#34;https://explore.garmin.com&#34;&gt;explore.garmin.com&lt;/a&gt;. Depending on your Garmin
subscription, sending check-in messages are free of charge. In the configuration section,
you can enable the option to include your latitude/longitude and a link to the
Garmin map in each SMS message. This information is always included for email
recipients&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quick Messages:&lt;/strong&gt; You can create up to 20 predefined messages so you don’t
have to type them while you’re on the trail. The number of free messages you
get depends on your Garmin subscription; any additional messages are billed per
use. You can create or edit these messages at explore.garmin.com.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Normal Messages:&lt;/strong&gt; In the Garmin Messenger iPhone app, you can type any custom
message and send it to both SMS and email recipients. These messages are billed
the same way as quick messages.&lt;/p&gt;
&lt;p&gt;You can configure the system to send all messages to any email/sms recipients. The great
thing is that the unlimited check-in messages also include latitude/longitude information.
Here is a sample message.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;Arrived at Camp

View the location or send a reply to Peter Hoffmann:
https://inreachlink.com/&amp;lt;unique_code&amp;gt;

Peter Hoffmann sent this message from: Lat 48.996386 Lon 8.468849

Do not reply directly to this message.

This message was sent to you using the inReach two-way satellite communicator with GPS. To learn more, visit http://explore.garmin.com/inreach.
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As we do not want to spam all our friends with daily checkins I have build
a little leaflet-checkin plugin and an imap scraper to pull and visualize
the checkin/messages.&lt;/p&gt;
&lt;h1&gt;Build your own Tracking with Check-In Messages&lt;/h1&gt;
&lt;p&gt;For battery life reasons, we are not interested in real-time live tracking.&lt;br /&gt;
Instead, I’ve created a small script that checks a dedicated IMAP email account
for check-in messages and publishes them to a server, which then displays the
location of our most recent check-in. Sending a check-in once a day or during
each break when we are in more remote areas—should give our friends enough
information in case any problems arise.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/static/2026/leaflet-checkin-map.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;A straightforward Python script connects to my IMAP server, retrieves all emails from
the Garmin InReach service, parses the message, timestamp, and latitude/longitude, and
then updates a &lt;code&gt;positions.json&lt;/code&gt; file on my webserver.&lt;/p&gt;
&lt;p&gt;Then a simple static html file with a &lt;a href=&#34;https://leafletjs.com&#34;&gt;leaflet&lt;/a&gt; map pulls
the &lt;code&gt;positions.json&lt;/code&gt; file and displays the messages/checkins on the map.&lt;/p&gt;
&lt;p&gt;A demo of the map is available at:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://hoffmann.github.io/garmin-inreach-checkin-map/html/map.html&#34;&gt;https://hoffmann.github.io/garmin-inreach-checkin-map/html/map.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;and you can checkout the code&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/hoffmann/garmin-inreach-checkin-map&#34;&gt;https://github.com/hoffmann/garmin-inreach-checkin-map&lt;/a&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;ch&#34;&gt;#!/usr/bin/env python3&lt;/span&gt;
&lt;span class=&#34;sd&#34;&gt;&amp;quot;&amp;quot;&amp;quot;Poll IMAP inbox for Garmin inReach emails and extract positions into positions.json.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;email&lt;/span&gt;
&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;email.utils&lt;/span&gt;
&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;imaplib&lt;/span&gt;
&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;json&lt;/span&gt;
&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;os&lt;/span&gt;
&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;re&lt;/span&gt;
&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;sys&lt;/span&gt;
&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;datetime&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;datetime&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;timezone&lt;/span&gt;

&lt;span class=&#34;n&#34;&gt;BOILERPLATE_PREFIXES&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
    &lt;span class=&#34;s2&#34;&gt;&amp;quot;View the location&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
    &lt;span class=&#34;s2&#34;&gt;&amp;quot;Do not reply&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
    &lt;span class=&#34;s2&#34;&gt;&amp;quot;This message was sent&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;

&lt;span class=&#34;n&#34;&gt;POSITIONS_FILE&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;os&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;path&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;join&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;os&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;path&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dirname&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;os&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;path&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;abspath&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;vm&#34;&gt;__file__&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)),&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;positions.json&amp;quot;&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;


&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;connect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;host&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;password&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;imap&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;imaplib&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;IMAP4_SSL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;host&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;imap&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;login&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;password&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;imap&lt;/span&gt;


&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;search_inreach_emails&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;imap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;imap&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;select&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;INBOX&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;status&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;imap&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;search&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
        &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;(OR FROM &amp;quot;no.reply.inreach@garmin.com&amp;quot; SUBJECT &amp;quot;inReach message&amp;quot;)&amp;#39;&lt;/span&gt;
    &lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;status&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;OK&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;msg_ids&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;split&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;msg_ids&lt;/span&gt;


&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;get_text_body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;msg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;msg&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;is_multipart&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;():&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;part&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;msg&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;walk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;():&lt;/span&gt;
            &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;part&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get_content_type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;text/plain&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
                &lt;span class=&#34;n&#34;&gt;charset&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;part&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get_content_charset&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;or&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;utf-8&amp;quot;&lt;/span&gt;
                &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;part&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get_payload&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;decode&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;True&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;decode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;charset&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;n&#34;&gt;charset&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;msg&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get_content_charset&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;or&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;utf-8&amp;quot;&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;msg&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get_payload&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;decode&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;True&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;decode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;charset&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;&amp;quot;&lt;/span&gt;


&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;parse_timestamp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;msg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;date_str&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;msg&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;Date&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;not&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;date_str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;dt&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;email&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utils&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;parsedate_to_datetime&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;date_str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;dt_utc&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dt&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;astimezone&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;timezone&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;utc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dt_utc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;strftime&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;%Y-%m-&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;%d&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;T%H:%M:%SZ&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;


&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;parse_body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;lines&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;strip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;splitlines&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;

    &lt;span class=&#34;c1&#34;&gt;# Extract message: first non-empty line&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;message&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;line&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lines&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;n&#34;&gt;stripped&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;line&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;strip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;stripped&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
            &lt;span class=&#34;n&#34;&gt;message&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;stripped&lt;/span&gt;
            &lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;

    &lt;span class=&#34;c1&#34;&gt;# Check if the message is boilerplate&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;any&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;startswith&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;prefix&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;prefix&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;BOILERPLATE_PREFIXES&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
        &lt;span class=&#34;n&#34;&gt;message&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class=&#34;c1&#34;&gt;# Extract lat/lon&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;lat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lon&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;m&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;re&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;search&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;sa&#34;&gt;r&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;Lat\s+([-\d.]+)\s+Lon\s+([-\d.]+)&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;n&#34;&gt;lat&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;float&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;group&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
        &lt;span class=&#34;n&#34;&gt;lon&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;float&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;group&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;

    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lon&lt;/span&gt;


&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;parse_email&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;msg_data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;msg&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;email&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;message_from_bytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;msg_data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;

    &lt;span class=&#34;n&#34;&gt;timestamp&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;parse_timestamp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;msg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;not&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;timestamp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;

    &lt;span class=&#34;n&#34;&gt;body&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;get_text_body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;msg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;not&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;

    &lt;span class=&#34;n&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lon&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;parse_body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lat&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;is&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;or&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lon&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;is&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;

    &lt;span class=&#34;n&#34;&gt;entry&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
        &lt;span class=&#34;s2&#34;&gt;&amp;quot;timestamp&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;timestamp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
        &lt;span class=&#34;s2&#34;&gt;&amp;quot;lat&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
        &lt;span class=&#34;s2&#34;&gt;&amp;quot;lon&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;lon&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;n&#34;&gt;entry&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;msg&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;message&lt;/span&gt;
    

    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;entry&lt;/span&gt;


&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;load_positions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;():&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;os&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;path&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;exists&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;POSITIONS_FILE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;with&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;open&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;POSITIONS_FILE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;f&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
            &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;json&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;load&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;f&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;


&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;save_positions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;positions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;with&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;open&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;POSITIONS_FILE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;w&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;as&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;f&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;n&#34;&gt;json&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dump&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;positions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;f&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;indent&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
        &lt;span class=&#34;n&#34;&gt;f&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;write&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;\n&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;


&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;main&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;():&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;host&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;os&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;environ&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;IMAP_HOST&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;user&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;os&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;environ&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;IMAP_USER&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;password&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;os&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;environ&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;IMAP_PASSWORD&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;

    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;not&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;all&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;([&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;host&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;password&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]):&lt;/span&gt;
        &lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;Error: Set IMAP_HOST, IMAP_USER, and IMAP_PASSWORD environment variables.&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
        &lt;span class=&#34;n&#34;&gt;sys&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;exit&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;

    &lt;span class=&#34;n&#34;&gt;imap&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;connect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;host&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;password&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;n&#34;&gt;msg_ids&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;search_inreach_emails&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;imap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
        &lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;sa&#34;&gt;f&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;Found &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;len&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;msg_ids&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt; inReach email(s)&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;

        &lt;span class=&#34;n&#34;&gt;new_entries&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;msg_id&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;msg_ids&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
            &lt;span class=&#34;n&#34;&gt;status&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;imap&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fetch&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;msg_id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;(RFC822)&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
            &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;status&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;OK&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
                &lt;span class=&#34;k&#34;&gt;continue&lt;/span&gt;
            &lt;span class=&#34;n&#34;&gt;entry&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;parse_email&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt;
            &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;entry&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
                &lt;span class=&#34;n&#34;&gt;new_entries&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;append&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;entry&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;finally&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;n&#34;&gt;imap&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;logout&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;

    &lt;span class=&#34;n&#34;&gt;existing&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;load_positions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;existing_timestamps&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;timestamp&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;p&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;existing&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;

    &lt;span class=&#34;n&#34;&gt;added&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;entry&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;new_entries&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;entry&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;timestamp&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;not&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;existing_timestamps&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
            &lt;span class=&#34;n&#34;&gt;existing&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;append&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;entry&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
            &lt;span class=&#34;n&#34;&gt;existing_timestamps&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;add&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;entry&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;timestamp&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt;
            &lt;span class=&#34;n&#34;&gt;added&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;

    &lt;span class=&#34;n&#34;&gt;existing&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sort&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;key&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;lambda&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;timestamp&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;save_positions&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;existing&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;

    &lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;sa&#34;&gt;f&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;Added &lt;/span&gt;&lt;span class=&#34;si&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;added&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt; new position(s) (&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;len&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;existing&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt; total)&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;


&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;vm&#34;&gt;__name__&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;main&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
</content>
  </entry>
  <entry xml:base="https://peter-hoffmann.com/feed/atom.xml">
    <title type="text">Local macOS Dev Setup: dnsmasq + caddy-snake for python projects</title>
    <id>https://peter-hoffmann.com/2026/local-mac-dev-setup-with-dnsmasq-and-caddy.html</id>
    <updated>2026-02-05T00:00:00Z</updated>
    <published>2026-02-05T00:00:00Z</published>
    <link href="/2026/local-mac-dev-setup-with-dnsmasq-and-caddy.html" />
    <author>
      <name>Peter Hoffmann</name>
      <uri>https://peter-hoffmann.com</uri>
    </author>
    <content type="html">&lt;p&gt;When working on a single web project, running &lt;code&gt;flask run&lt;/code&gt; on a fixed port is
usually more than sufficient. However, as soon as you start developing
&lt;strong&gt;multiple services in parallel&lt;/strong&gt;, this approach quickly becomes cumbersome:
ports collide, you have to remember which service runs on which port, and you
end up constantly starting, stopping, and restarting individual development
servers by hand.&lt;/p&gt;
&lt;p&gt;Using a wildcard local domain (&lt;code&gt;*.lan&lt;/code&gt;) throuhg dnsmask and a vhost proxy with
proper WSGI services solves these problems cleanly. Each project gets a stable,
memorable local subdomain instead of a port number, services can run side by
side without collisions, and process management becomes centralized and
predictable. The result is a local development setup with less friction.&lt;/p&gt;
&lt;h1&gt;dnsmasq on macOS Sonoma (Local DNS with &lt;code&gt;.lan&lt;/code&gt;)&lt;/h1&gt;
&lt;p&gt;This is a concise summary of how to install and configure &lt;strong&gt;dnsmasq&lt;/strong&gt; on &lt;strong&gt;macOS
Sonoma&lt;/strong&gt; to resolve local development domains using a &lt;code&gt;.lan&lt;/code&gt; wildcard (e.g.
&lt;code&gt;*.lan → 127.0.0.1&lt;/code&gt;).&lt;/p&gt;
&lt;h2&gt;1. Install dnsmasq&lt;/h2&gt;
&lt;p&gt;Using Homebrew:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;brew&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;install&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;dnsmasq
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Homebrew (on Apple Silicon) installs dnsmasq and places the default config in:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;/opt/homebrew/etc/dnsmasq.conf
&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;2. Configure dnsmasq&lt;/h2&gt;
&lt;p&gt;Edit the configuration file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;vim&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;/opt/homebrew/etc/dnsmasq.conf
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Add the following:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;# Listen only on localhost
listen-address=127.0.0.1
bind-interfaces

# DNS port
port=53

# Wildcard domain for local development
address=/.lan/127.0.0.1
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This maps &lt;strong&gt;any &lt;code&gt;*.lan&lt;/code&gt; hostname&lt;/strong&gt; to &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It&#39;s recommended to not use &lt;code&gt;.dev&lt;/code&gt; as this is real Google owned TLD and browsers
have baked in to use HTTPS only. Also don&#39;t use &lt;code&gt;.local&lt;/code&gt; as this is reserved for
mDNS (Bonjour).&lt;/p&gt;
&lt;h2&gt;3. Tell macOS to use dnsmasq&lt;/h2&gt;
&lt;p&gt;macOS ignores &lt;code&gt;/etc/resolv.conf&lt;/code&gt;, so DNS must be configured per network interface.&lt;/p&gt;
&lt;h3&gt;Option A: System Settings (GUI)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;System Settings → Network&lt;/li&gt;
&lt;li&gt;Select your active interface (Wi-Fi / Ethernet)&lt;/li&gt;
&lt;li&gt;Details → DNS&lt;/li&gt;
&lt;li&gt;Add:&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;127.0.0.1
&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;Move it to the &lt;strong&gt;top&lt;/strong&gt; of the DNS server list&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Option B: Command line&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;networksetup&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;-setdnsservers&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;Wi-Fi&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;m&#34;&gt;127&lt;/span&gt;.0.0.1
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(Replace &lt;code&gt;Wi-Fi&lt;/code&gt; with the correct interface name if needed.)&lt;/p&gt;
&lt;h2&gt;4. Start dnsmasq&lt;/h2&gt;
&lt;p&gt;Run it as a background service:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;brew&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;services&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;start&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;dnsmasq
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Or run it manually for debugging:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;dnsmasq&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;--no-daemon
&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;5. Flush DNS cache&lt;/h2&gt;
&lt;p&gt;This step is required on Sonoma:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;sudo&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;dscacheutil&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;-flushcache
sudo&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;killall&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;-HUP&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;mDNSResponder
&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;6. Test&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;dig&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;foo.lan
ping&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;foo.lan
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Both should resolve to:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;127.0.0.1
&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Result&lt;/h2&gt;
&lt;p&gt;You now have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dnsmasq running on &lt;code&gt;127.0.0.1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Wildcard local DNS via &lt;code&gt;*.lan&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Fully compatible behavior with macOS Sonoma&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Caddy&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; My initial plan was to use caddy with caddy-snake to run multiple
vhosts for python wsgi apps with the configuration below. But this did
not work out as expected because caddy-snake does not run multiple python
interpreters for the different projects, but &lt;strong&gt;only&lt;/strong&gt; appends the site packages
from the python projects to &lt;code&gt;sys.path&lt;/code&gt; for all projects and runs all of them in
the same python interpreter. This leads to problems with different python
versions or incompatible python requirements installed in the different venv
versions. So the approach below only works if you use the same python version
and your requirements are compatible within the different apps.&lt;/p&gt;
&lt;h2&gt;Caddyfile: host multiple WSGI services&lt;/h2&gt;
&lt;p&gt;As we want caddy to run wsgi services we need to build &lt;a href=&#34;https://github.com/mliezun/caddy-snake&#34;&gt;caddy-snake&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;The caddyfile now needs to be stored in &lt;code&gt;$(brew --prefix)/etc/Caddyfile&lt;/code&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;{
    auto_https off
}

http://foo.lan {
    bind 127.0.0.1
    route {
        python {
            module_wsgi app:app
            working_dir /Users/you/dev/foo
            venv /Users/you/dev/foo/.venv
		}
	}

    log {
        output stdout
        format console
    }
}

http://bar.lan {
    bind 127.0.0.1 
    route {
        python {
            module_wsgi app:app
            working_dir /Users/you/dev/bar
            venv /Users/you/dev/bar/.venv
        }
    }
}
&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Update&lt;/h2&gt;
&lt;p&gt;Miguel, the author of &lt;a href=&#34;https://github.com/mliezun/caddy-snake&#34;&gt;caddy-snake&lt;/a&gt;
reached out, recently he released a &lt;a href=&#34;https://caddy-snake.readthedocs.io/en/latest/blog/dynamic-modules-autoreload/&#34;&gt;new version&lt;/a&gt;
of caddy-snake that allows dynamic modules, it will make the local dev setup
much easier, one can now write something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;{
    auto_https off
}

http://*.lan {
    bind 127.0.0.1
    route {
        python {
            module_wsgi app:app
            working_dir /Users/you/dev/{http.request.host.labels.2}
            venv /Users/you/dev/{http.request.host.labels.2}/.venv
	}
     }

    log {
        output stdout
        format console
    }
}
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now it&#39;s possible to select working dir and venv based on portions of the host used in the request, which means you can have a single caddyfile, that you don&#39;t need to modify, for all your apps.&lt;/p&gt;
&lt;p&gt;This is possible thanks to added support for caddy &lt;a href=&#34;https://caddyserver.com/docs/conventions#placeholders&#34;&gt;placeholders&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry xml:base="https://peter-hoffmann.com/feed/atom.xml">
    <title type="text">Georg Bayerle - Der Alpen Appell</title>
    <id>https://peter-hoffmann.com/2025/georg-bayerle-der-alpen-appell.html</id>
    <updated>2025-09-30T00:00:00Z</updated>
    <published>2025-09-30T00:00:00Z</published>
    <link href="/2025/georg-bayerle-der-alpen-appell.html" />
    <author>
      <name>Peter Hoffmann</name>
      <uri>https://peter-hoffmann.com</uri>
    </author>
    <content type="html">&lt;p&gt;In seinem Buch &lt;a href=&#34;https://www.tyroliaverlag.at/item/75729918&#34;&gt;Der Alpen Appell&lt;/a&gt; schildert Georg Bayerle eindrucksvoll anhand von vielen Beispielen, wie der Klimawandel und die zunehmende touristische sowie wirtschaftliche Erschließung den Naturraum Alpen verändern und belasten.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/static/2025/alpen-apell.jpg&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;Die rhetorische Frage &amp;quot;Dürfen wir jetzt noch Ski fahren?&amp;quot; beantwortet Georg Bayerle wie folgt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Natürlich dürfen wir. Dieses Buch möchte vielmehr bewirken, dass Skifahren noch lange
erhalten bleibt, indem sich der Skisport und wir alle besser an die Natur und
die natürlichen Bedingungen anpassen. Dazu gehört, den Klimawandel nicht mit immer noch
größerem Ressourceneinsatz anzuheizen, indem wir dessen Folgen bekämpfen, sondern sich auf die
Veränderungen einzulassen.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Die Alpen sind seit jeher Sehnsuchtsort, Rückzugsraum und Sinnbild für Ursprünglichkeit. Doch was passiert, wenn dieser natürliche Raum – dieses fragile Ökosystem – zunehmend unter der Last von Tourismus, Erschließung und Klimawandel ächzt? In Georg Bayerles Streitschrift &amp;quot;Der Alpen-Appell&amp;quot; schlägt der Autor einen ehrlichen, mahnenden Ton an: Die Alpen dürfen nicht zum „Funpark“ verkommen. Vielmehr muss ein Umdenken einsetzen, um die Berge für kommende Generationen zu bewahren.&lt;/p&gt;
&lt;p&gt;Das Buch ist keine theoretische Abhandlung, sondern stellt eine engagierte und sehr konkrete Bestandsaufnahme dar: Bayerle kombiniert persönliche Beobachtungen mit Fallbeispielen, Fachrecherchen und reflektierenden Überlegungen. Er zeigt auf, wie der wirtschaftliche Druck, touristische Begehrlichkeiten und klimatische Veränderungen zusammen unsere Alpenlandschaft und ihre Strukturen verändern.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fragiles Gleichgewicht unter Druck&lt;/strong&gt;
Bayerle macht darauf aufmerksam, dass viele alpine Lebensräume bereits an ihre Belastungsgrenzen stoßen – ob durch Bodenversiegelung, Ausbau von Liftanlagen, Straßen, Schneekanonen oder Speicherseen. Besonders problematisch, so Bayerle, ist es, wenn technische Lösungen zur Norm werden und natürliche Bedingungen damit außer Kraft gesetzt werden.
So bemerkt er, dass künstlich produzierter Schnee oft als natürlich wahrgenommen wird und Speicherbecken wie &amp;quot;Kopien&amp;quot; von Bergseen inszeniert werden, ohne Rücksicht auf die dabei verursachten Eingriffe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Konfliktfeld Erschließung vs. Schutz&lt;/strong&gt;
Ein wiederkehrendes Thema ist der Ausbau von Seilbahnen, Liften und infrastruktureller Erschließung – insbesondere in fragilen Gebieten. Bayerle kritisiert, dass trotz langjähriger Forderungen, zum Beispiel von Alpenvereinen und der Alpenschutzorganisation CIPRA, oft weitergebaut wird. Beispiele hierfür sind Tirol oder das Grenzgebiet zwischen dem Ötztal und dem Pitztal.&lt;/p&gt;
&lt;p&gt;Er zeigt zudem auf, wie manche Skigebiete in ihrer Ausgestaltung bereits in den Bereich von Freizeitparks oder Themendörfern übergehen – mit all den infrastrukturellen Eingriffen, die damit verbunden sind.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Konkrete Lösungsansätze und Reflexionen&lt;/strong&gt;
Bayerle beschränkt sich jedoch nicht auf reine Anklagen – er sucht nach Orientierungshilfen für eine nachhaltigere Zukunft. Er verweist auf Initiativen wie die Bergsteigerdörfer, den Ansatz der „Alpine Pearls“ oder Modelle des sanften Tourismus, die das Ziel verfolgen, Natur- und Erlebnisraum in Einklang zu bringen.&lt;/p&gt;
&lt;p&gt;Zentral ist dabei der Appell an mehr Achtsamkeit – sowohl bei Gästen als auch bei Einheimischen und Entscheidungsträgern. Denn nur ein sensiblerer Umgang mit den Landschaften bietet die Chance, Erholung und Nachhaltigkeit miteinander zu verbinden.&lt;/p&gt;
&lt;h2&gt;Weiterführendes Material&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Bergauf Bergab: Kraftwerk Kühtai: Das Längental wird zum Speichersee&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Im Bergauf-Bergab-Beitrag wird eindrücklich gezeigt, wie alpiner Raum durch
Wasserkraftprojekte und Speicherbecken verändert wird.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=xHuYMXcCBW8&#34;&gt;https://www.youtube.com/watch?v=xHuYMXcCBW8&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Alexander Schiebel: Das Wunder von Mals&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Die Dokumentation schildert, wie eine Gemeinde gegen Pestizide und industrielle Landwirtschaft
kämpft – ein Beispiel, wie lokale Selbstbestimmung gegen externe ökonomische
Interessen wirken kann.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=HUoluVn7xbI&#34;&gt;https://www.youtube.com/watch?v=HUoluVn7xbI&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Alpenverein Basecamp Podcast: #050 Der Alpen-Appell: Wie retten wir die Alpen?&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unsere Jubiläumsfolge haben wir am Alpenklimagipfel direkt am Gipfel der
Zugspitze aufgenommen. Auf 2.962 Metern diskutieren der preisgekrönte
Umweltjournalist Georg Bayerle und der Gletscherbahn-Manager Reinhard Klier über
die Zukunft der Alpen. Wie können wir das Gleichgewicht zwischen Erleben und
Erhalten, zwischen Wirtschaft und Wildnis neu austarieren?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Reinhard Klier: Geologe, Vorstand der Wintersport Tirol AG und Obmann der
Fachgruppe Seilbahnen in der Wirtschaftskammer Tirol. Vertritt rund 600
Mitarbeiter:innen und sieht in der Erschließung auch Verantwortung,
Arbeitsplätze und regionalen Zusammenhalt.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://alpenverein-basecamp.podigee.io/50-alpen-appell&#34;&gt;https://alpenverein-basecamp.podigee.io/50-alpen-appell&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bergauf Bergab: Pitztal und Ötztal: Bedrohte Winterparadiese&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Obwohl die Alpenvereine und auch die Internationale Alpenschutzkommission Cipra
schon seit Jahren einen Erschließungsstopp in den Alpen fordern, geht der Ausbau
der Seilbahnen und Lifte weiter – ganz besonders intensiv in unserem Nachbarland
Tirol. Besonders große Aufregung ruft derzeit die so genannte „Gletscherehe“
zwischen dem Ötztal und dem Pitztal hervor. Dabei geht es um das Gebiet um den
Linken Fernerkogel und die Braunschweiger Hütte, sehr bekannt, weil im Sommer
der Europäische Fernwanderweg E5 hindurchführt – ein Gelände, das komplett mit
Liften erschlossen werden würde. Georg Bayerle hat sich das Gebiet, um das es
geht, genau angesehen und wirft aus bergsteigerischer Perspektive einen Blick
auf die Erschließungspläne!Welchen Wert wilde und unberührte Gegenden haben,
zeigt ein zweiter Beitrag, bei dem &amp;quot;Bergauf-Bergab&amp;quot; eine Gruppe junger
Snowboarderinnen und Snowboarder in die Leventina begleitet hat, eine einsame
Gegend in der Schweiz südlich des Gotthard-Tunnels.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://www.ardmediathek.de/video/Y3JpZDovL2JyLmRlL3ZpZGVvLzM4NjBjNGFjLWNiNjAtNDhjZS05OTJjLWQ5M2Y0YzE1ZDE3Mg&#34;&gt;https://www.ardmediathek.de/video/Y3JpZDovL2JyLmRlL3ZpZGVvLzM4NjBjNGFjLWNiNjAtNDhjZS05OTJjLWQ5M2Y0YzE1ZDE3Mg&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ARD Radiofeature: Der Alpenkollaps - Der Journalist Georg Bayerle im Gespräch&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Die Olympischen Winterspiele 2026 in den italienischen Dolomiten sollen
klimafreundlich und nachhaltig werden, versprechen die Organisatoren. Doch es
droht ein Desaster: Kunstschnee, Straßenbau und Massentourismus. Dabei sorgt der
Klimawandel schon jetzt für Gletscherschmelze, Sturzfluten und Felsabbrüche in
den Alpen. BR-Journalist und Bergkenner Georg Bayerle hat mehr als ein Jahr zu
den Vorgängen recherchiert. Im Podcast mit Palina Milling erzählt er über die
Zivilcourage der Bewohner, die Ignoranz der Funktionäre und die Ideen, wie man
Sportveranstaltungen nachhaltig und umweltverträglich gestalten kann&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://www.ardaudiothek.de/episode/urn:ard:section:9ffc2a089a9fcd14/&#34;&gt;https://www.ardaudiothek.de/episode/urn:ard:section:9ffc2a089a9fcd14/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tagesschau Podcast: Zukunft der Alpen: Wie das Klima Berge bröckeln lässt&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Das Leben in den #Alpen war noch nie ungefährlich. Die Naturgewalt der Berge
prägt die Menschen und ihren Alltag dort seit jeher. Doch die Schlagzahl und
Intensität von #Bergstürzen, Schlammlawinen und Muren hat durch die
Klimaerwärmung zugenommen und macht das Leben in den Bergen gefährlicher.
BR-Journalist und Bergexperte Georg Bayerle erzählt in dieser Folge von
Einheimischen, deren Heimat verschwindet und Forschenden, die nach Mitteln und
Wegen suchen, die Gefahr in Zukunft zu begrenzen. Wie wird sich das Leben in den
Alpen verändern?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=aZ1oQlOYWc8&amp;amp;pp=ygUNZ2VvcmcgYmF5ZXJsZQ%3D%3D&#34;&gt;https://www.youtube.com/watch?v=aZ1oQlOYWc8&amp;amp;pp=ygUNZ2VvcmcgYmF5ZXJsZQ%3D%3D&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bayrischer Rundfunk: Muren und Sturzfluten: Orte in den Alpen kämpfen um ihre Zukunft&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Überall in den Alpen bedrohen Sturzfluten, Muren und Schlammlawinen die
Menschen und ihre Heimat und richten gewaltige Schäden an. Das geht nicht erst
seit den dramatischen Bildern aus dem Schweizer Lötschental durch die
Schlagzeilen, wo im Mai dieses Jahres das Bergdorf Blatten von einer Mure
verschüttet wurde. Doch wie können sich die Orte besser schützen?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=nhF9D4FyjsI&#34;&gt;https://www.youtube.com/watch?v=nhF9D4FyjsI&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  <entry xml:base="https://peter-hoffmann.com/feed/atom.xml">
    <title type="text">Mistune 3 Wikilink Inline Parser</title>
    <id>https://peter-hoffmann.com/2025/mistune-3-wikilink-inline-parser.html</id>
    <updated>2025-09-20T00:00:00Z</updated>
    <published>2025-09-20T00:00:00Z</published>
    <link href="/2025/mistune-3-wikilink-inline-parser.html" />
    <author>
      <name>Peter Hoffmann</name>
      <uri>https://peter-hoffmann.com</uri>
    </author>
    <content type="html">&lt;p&gt;&lt;a href=&#34;https://mistune.lepture.com/&#34;&gt;Mistune 3&lt;/a&gt; has changed its internal structure and extension mechanisms.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://mistune.lepture.com/en/latest/advanced.html&#34;&gt;Mistune advanced documentation&lt;/a&gt; provides several examples of how to create and register inline patterns. After a few iterations, I developed the following solution to render wikilinks:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;re&lt;/span&gt;  
&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;typing&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Match&lt;/span&gt;  
  
&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;mistune&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Markdown&lt;/span&gt;  
&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;mistune.core&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;BaseRenderer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;InlineState&lt;/span&gt;  
&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;mistune.inline_parser&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;InlineParser&lt;/span&gt;  

  
&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;parse_wikilink&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;inline&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;InlineParser&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Match&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;InlineState&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;int&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;  
&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;sd&#34;&gt;&amp;quot;&amp;quot;&amp;quot;Parse wikilink syntax [[page title]] or [[page title|display text]].&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;  
    &lt;span class=&#34;n&#34;&gt;page_title&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;group&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;page&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;strip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;  
    &lt;span class=&#34;n&#34;&gt;display_text&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;group&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;display&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;  
      
    &lt;span class=&#34;c1&#34;&gt;# Use display text if provided; otherwise, use page title  &lt;/span&gt;
    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;display_text&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;is&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;not&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;  
        &lt;span class=&#34;n&#34;&gt;text&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;display_text&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;strip&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;  
    &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;  
        &lt;span class=&#34;n&#34;&gt;text&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;page_title&lt;/span&gt;  
      
    &lt;span class=&#34;c1&#34;&gt;# Create a wikilink token  &lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;append_token&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt;  
        &lt;span class=&#34;s2&#34;&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;wikilink&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;  
        &lt;span class=&#34;s2&#34;&gt;&amp;quot;children&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[{&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;raw&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}],&lt;/span&gt;  
        &lt;span class=&#34;s2&#34;&gt;&amp;quot;attrs&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;  
            &lt;span class=&#34;s2&#34;&gt;&amp;quot;page&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;page_title&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;  
            &lt;span class=&#34;s2&#34;&gt;&amp;quot;display&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;text&lt;/span&gt;  
        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;  
    &lt;span class=&#34;p&#34;&gt;})&lt;/span&gt;  
      
    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;end&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;  
  
&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;render_wikilink_html&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;renderer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;BaseRenderer&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;children&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;page&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;display&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;  
&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;sd&#34;&gt;&amp;quot;&amp;quot;&amp;quot;Render a wikilink as an HTML anchor tag.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;  
    &lt;span class=&#34;n&#34;&gt;url_page&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;page&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;replace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;_&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;  
    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;sa&#34;&gt;f&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;&amp;lt;a href=&amp;quot;/wiki/&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;url_page&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;quot; class=&amp;quot;wikilink&amp;quot;&amp;gt;&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;children&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;lt;/a&amp;gt;&amp;#39;&lt;/span&gt;  
  
&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;wikilink_plugin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;md&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;Markdown&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;  
&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;sd&#34;&gt;&amp;quot;&amp;quot;&amp;quot;Plugin function to add wikilink support to Mistune.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;  
    &lt;span class=&#34;c1&#34;&gt;# Use named groups&lt;/span&gt;
    &lt;span class=&#34;n&#34;&gt;WIKILINK_PATTERN&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;sa&#34;&gt;r&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;\[\[(?P&amp;lt;page&amp;gt;[^|\]]+)(?:\|(?P&amp;lt;display&amp;gt;[^\]]+))?\]\]&amp;#39;&lt;/span&gt;  
    &lt;span class=&#34;n&#34;&gt;md&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;inline&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;register&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;wikilink&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;WIKILINK_PATTERN&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;parse_wikilink&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;before&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;link&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;  
      
    &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;md&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;renderer&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;and&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;md&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;renderer&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;NAME&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;html&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;  
        &lt;span class=&#34;n&#34;&gt;md&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;renderer&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;register&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;wikilink&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;render_wikilink_html&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To test it, you can run the following code:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;mistune&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;create_markdown&lt;/span&gt;  
    
&lt;span class=&#34;c1&#34;&gt;# Create a Markdown parser with the wikilink plugin  &lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;md&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;create_markdown&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;plugins&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;wikilink_plugin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt;  
    
&lt;span class=&#34;c1&#34;&gt;# Test examples  &lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;text1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;Check out [[Main Page]] for more info.&amp;quot;&lt;/span&gt;  
&lt;span class=&#34;n&#34;&gt;text2&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;Visit [[Installation Guide|the installation guide]] to get started.&amp;quot;&lt;/span&gt;  
&lt;span class=&#34;n&#34;&gt;text3&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;Multiple links: [[Page One]] and [[Page Two|Custom Text]].&amp;quot;&lt;/span&gt;  
    
&lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;Example 1:&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;  
&lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;md&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;  
&lt;span class=&#34;c1&#34;&gt;# Output: &amp;lt;p&amp;gt;Check out &amp;lt;a href=&amp;quot;/wiki/Main_Page&amp;quot; class=&amp;quot;wikilink&amp;quot;&amp;gt;Main Page&amp;lt;/a&amp;gt; for more info.&amp;lt;/p&amp;gt;  &lt;/span&gt;
    
&lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;\n&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;Example 2:&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;  
&lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;md&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;  
&lt;span class=&#34;c1&#34;&gt;# Output: &amp;lt;p&amp;gt;Visit &amp;lt;a href=&amp;quot;/wiki/Installation_Guide&amp;quot; class=&amp;quot;wikilink&amp;quot;&amp;gt;the installation guide&amp;lt;/a&amp;gt; to get started.&amp;lt;/p&amp;gt;  &lt;/span&gt;
    
&lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;\n&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;Example 3:&amp;quot;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;  
&lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;md&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;text3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;  
&lt;span class=&#34;c1&#34;&gt;# Output: &amp;lt;p&amp;gt;Multiple links: &amp;lt;a href=&amp;quot;/wiki/Page_One&amp;quot; class=&amp;quot;wikilink&amp;quot;&amp;gt;Page One&amp;lt;/a&amp;gt; and &amp;lt;a href=&amp;quot;/wiki/Page_Two&amp;quot; class=&amp;quot;wikilink&amp;quot;&amp;gt;Custom Text&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
</content>
  </entry>
  <entry xml:base="https://peter-hoffmann.com/feed/atom.xml">
    <title type="text">BikeRouter and OnRouteMap</title>
    <id>https://peter-hoffmann.com/2025/bikerouter-and-onroutemap.html</id>
    <updated>2025-09-19T00:00:00Z</updated>
    <published>2025-09-19T00:00:00Z</published>
    <link href="/2025/bikerouter-and-onroutemap.html" />
    <author>
      <name>Peter Hoffmann</name>
      <uri>https://peter-hoffmann.com</uri>
    </author>
    <content type="html">&lt;p&gt;Since following this year&#39;s &lt;a href=&#34;https://dotwatcher.cc/race/transcontinental-race-no11-2025&#34;&gt;transcontinental race&lt;/a&gt; and listening to the &lt;a href=&#34;https://www.christophstrasser.at/sitzfleisch_podcast/&#34;&gt;Sitzfleisch podcast&lt;/a&gt; by ultracycling professional Christoph Strasser and his peer Florian Kraschitzer, I became interested in long-distance cycling again. Planning long-distance cycling routes goes beyond simply connecting two points on a map. Besides finding a good route that balances distance and elevation gain, resupply is key. Food, water, and even spare parts must be planned for. Routes often require mapping out supermarkets, gas stations, or villages that can provide essential refueling points.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;http://bikerouter.de&#34;&gt;Bikerouter.de&lt;/a&gt; is an online tool for planning cycling routes with a strong focus on flexibility and detailed customization. At its core, it allows you to choose different routing profiles depending on the type of ride you are planning—whether that is road cycling, gravel, trekking, mountain biking, or simply the shortest or safest path between two points. For any route you calculate, the site can also suggest up to three alternatives, making it easy to compare different options before deciding which one fits your needs.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/static/2025/bikerouter.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;Routes can be built interactively by placing waypoints on the map, but you can also import existing tracks in formats like GPX, KML, or GeoJSON. Another powerful feature is the use of “no-go areas,” which can be drawn directly on the map or imported from a file; these tell the routing engine to avoid certain regions, such as busy roads or restricted trails. Once a route is ready, it can be exported in multiple formats—including GPX, FIT, CSV, and GeoJSON—or shared quickly through a shortlink or QR code.&lt;/p&gt;
&lt;p&gt;The site does not just create routes; it also provides rich analysis. For every planned trip, Bikerouter.de shows key statistics such as distance, riding time, total ascent, maximum elevation, and a detailed elevation profile. There are even more advanced stats like “plain ascent” (which distinguishes flat from climbing sections) and energy estimates, which give a deeper understanding of how demanding the ride might be.&lt;/p&gt;
&lt;p&gt;Behind the scenes, Bikerouter.de is powered by the &lt;a href=&#34;https://github.com/abrensch/brouter&#34;&gt;BRouter&lt;/a&gt; routing engine with map data from OpenStreetMap and elevation data from sources like the CGIAR-CSI SRTM dataset.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://onroutemap.de&#34;&gt;OnRouteMap&lt;/a&gt; is a tool to plan better by finding useful stops along your route. You start by uploading a GPX file of your planned route. Once uploaded, the website identifies “points of interest” (POIs) such as supermarkets, cafés, public drinking water sources, bakeries, fast food spots, and more. Optional categories include bicycle repair shops, shelters, campsites, hotels, or other accommodations. All these are shown in proximity to the route.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/static/2025/onroutemap.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;On top of that, you can download the GPX file again, now enriched with the points of interest (POIs), so that you can store it on your phone for offline use.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/static/2025/onroutemap-gpx.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;h2&gt;Similar Services&lt;/h2&gt;
&lt;p&gt;The discussion in &lt;a href=&#34;https://www.reddit.com/r/ultracycling/comments/1pi7noz/terrified_to_share_this_but_im_building_a_race/&#34;&gt;r/Ultracycling&lt;/a&gt; mentioned other services similar to OnRouteMap:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://my.ultramate.app/&#34;&gt;Ultramate.app&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://pitstopper.net&#34;&gt;pitstopper.net&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://getnavelo.com&#34;&gt;Navelo App&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry xml:base="https://peter-hoffmann.com/feed/atom.xml">
    <title type="text">Using SQLite with JSON Data</title>
    <id>https://peter-hoffmann.com/2024/using-sqlite-with-json-data.html</id>
    <updated>2024-10-13T00:00:00Z</updated>
    <published>2024-10-13T00:00:00Z</published>
    <link href="/2024/using-sqlite-with-json-data.html" />
    <author>
      <name>Peter Hoffmann</name>
      <uri>https://peter-hoffmann.com</uri>
    </author>
    <content type="html">&lt;h1&gt;Using SQLite with JSON Data&lt;/h1&gt;
&lt;p&gt;SQLite does not have a native &lt;code&gt;JSON&lt;/code&gt; column type like PostgreSQL. Instead, JSON is stored as &lt;code&gt;TEXT&lt;/code&gt; (or sometimes &lt;code&gt;BLOB&lt;/code&gt;), and queried or modified using SQLite’s JSON1 extension. Make sure your SQLite build includes JSON1 (for example, run: &lt;code&gt;SELECT json(&#39;null&#39;);&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Key points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Store JSON in a &lt;code&gt;TEXT&lt;/code&gt; column.&lt;/li&gt;
&lt;li&gt;Ensure JSON validity with &lt;code&gt;json_valid()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use JSON1 functions such as &lt;code&gt;json_extract&lt;/code&gt;, &lt;code&gt;json_set&lt;/code&gt;, and &lt;code&gt;json_each&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;You can index JSON paths using expression indexes for performance.&lt;/li&gt;
&lt;li&gt;If you use STRICT tables (SQLite 3.37+), declaring a column as &lt;code&gt;JSON&lt;/code&gt; enforces valid JSON automatically.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Example: Blog posts with arbitrary metadata&lt;/h2&gt;
&lt;h3&gt;Table schema&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;blogpost&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;INTEGER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;PRIMARY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;KEY&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;slug&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;TEXT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;UNIQUE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;title&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;     &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;TEXT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;TEXT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;TEXT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DEFAULT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;{}&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;

&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;-- Ensure metadata always contains valid JSON&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;CHECK&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_valid&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;metadata&lt;/code&gt; column can store any structure you like: tags, SEO data, flags, analytics, experiments, etc.&lt;/p&gt;
&lt;h2&gt;Inserting data&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;INSERT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;INTO&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;blogpost&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;slug&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;title&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;body&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;VALUES&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;sqlite-json&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;Using SQLite with JSON&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;...&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;{&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    &amp;quot;tags&amp;quot;: [&amp;quot;sqlite&amp;quot;, &amp;quot;json&amp;quot;],&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    &amp;quot;reading_time_min&amp;quot;: 7,&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    &amp;quot;seo&amp;quot;: {&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;      &amp;quot;title&amp;quot;: &amp;quot;SQLite JSON Guide&amp;quot;,&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;      &amp;quot;canonical&amp;quot;: &amp;quot;https://example.com/sqlite-json&amp;quot;&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    },&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    &amp;quot;draft&amp;quot;: false,&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    &amp;quot;published_at&amp;quot;: &amp;quot;2025-12-01&amp;quot;,&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    &amp;quot;views&amp;quot;: 1234&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;  }&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;draft-post&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;Unreleased Ideas&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;...&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;{&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    &amp;quot;tags&amp;quot;: [&amp;quot;notes&amp;quot;],&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    &amp;quot;draft&amp;quot;: true,&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    &amp;quot;assigned_to&amp;quot;: &amp;quot;alex&amp;quot;,&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    &amp;quot;experiments&amp;quot;: [&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;      {&amp;quot;name&amp;quot;: &amp;quot;A/B&amp;quot;, &amp;quot;variant&amp;quot;: &amp;quot;B&amp;quot;}&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    ]&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;  }&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Using &lt;code&gt;json(&#39;...&#39;)&lt;/code&gt; helps normalize the JSON and ensures validity.&lt;/p&gt;
&lt;h2&gt;Querying JSON data&lt;/h2&gt;
&lt;h3&gt;Extract a single value&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;title&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_extract&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;$.reading_time_min&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AS&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;reading_time_min&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;blogpost&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Filter by a JSON boolean (draft posts)&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;slug&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;title&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;blogpost&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_extract&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;$.draft&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;JSON &lt;code&gt;true&lt;/code&gt; and &lt;code&gt;false&lt;/code&gt; are typically returned as &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Query nested fields&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;slug&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;blogpost&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_extract&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;$.seo.title&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;LIKE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%Guide%&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Query array contents (posts tagged with &lt;code&gt;&amp;quot;json&amp;quot;&lt;/code&gt;)&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DISTINCT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;slug&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;title&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;blogpost&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AS&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;b&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;JOIN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_each&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;$.tags&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AS&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;t&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;t&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;json&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;json_each()&lt;/code&gt; expands a JSON array into rows.&lt;/p&gt;
&lt;h3&gt;Numeric comparisons on JSON values&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;slug&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_extract&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;$.views&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AS&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;views&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;blogpost&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_extract&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;$.views&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1000&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Check if a key exists&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;slug&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;blogpost&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;$.seo.canonical&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;IS&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;json_type()&lt;/code&gt; returns &lt;code&gt;NULL&lt;/code&gt; if the path does not exist.&lt;/p&gt;
&lt;h2&gt;Updating JSON data&lt;/h2&gt;
&lt;h3&gt;Add or update a key&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;UPDATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;blogpost&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;SET&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_set&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;$.views&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2000&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;slug&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;sqlite-json&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Remove a key&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;UPDATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;blogpost&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;SET&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_remove&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;$.assigned_to&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;slug&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;draft-post&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Append a value to a JSON array&lt;/h3&gt;
&lt;p&gt;SQLite does not have a simple “append” operator, so arrays are often rebuilt explicitly:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;UPDATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;blogpost&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;SET&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_set&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;$.tags&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_group_array&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_each&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;$.tags&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;      &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;UNION&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ALL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;sqlite&amp;#39;&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;slug&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;draft-post&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Use &lt;code&gt;UNION&lt;/code&gt; (instead of &lt;code&gt;UNION ALL&lt;/code&gt;) to avoid duplicates.&lt;/p&gt;
&lt;h2&gt;Indexing JSON for performance&lt;/h2&gt;
&lt;p&gt;If you frequently query a JSON path, create an expression index:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;INDEX&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;idx_blogpost_draft&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;ON&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;blogpost&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_extract&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;$.draft&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;

&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;INDEX&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;idx_blogpost_published_at&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;ON&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;blogpost&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_extract&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;metadata&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;$.published_at&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;These indexes allow SQLite to efficiently filter on JSON values.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Store JSON in a &lt;code&gt;TEXT&lt;/code&gt; column.&lt;/li&gt;
&lt;li&gt;Enforce validity with &lt;code&gt;json_valid()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Query with &lt;code&gt;json_extract&lt;/code&gt;, &lt;code&gt;json_each&lt;/code&gt;, and &lt;code&gt;json_type&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Update parts of JSON using &lt;code&gt;json_set&lt;/code&gt; and &lt;code&gt;json_remove&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use expression indexes for frequently queried JSON paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach works well for flexible, evolving metadata such as blog post attributes, feature flags, or analytics data.&lt;/p&gt;
&lt;h2&gt;Further Information&lt;/h2&gt;
&lt;p&gt;The blog post &lt;a href=&#34;https://www.dbpro.app/blog/sqlite-json-virtual-columns-indexing&#34;&gt;SQLite JSON Virtual Columns and Indexing&lt;/a&gt;
explains the concept of virtual columns in SQLite, which allow you to create computed
columns based on JSON extraction expressions. The article also covers how to
efficiently index these virtual columns for better performance when querying
JSON fields in your tables, with examples demonstrating
schema design, creating indexes, and writing efficient queries with SQLite&#39;s
built-in JSON support. The corresponding &lt;a href=&#34;https://news.ycombinator.com/item?id=46243904&#34;&gt;Hacker News Discussion&lt;/a&gt;
has more information.&lt;/p&gt;
&lt;p&gt;You can use &lt;a href=&#34;https://sqliteonline.com&#34;&gt;https://sqliteonline.com&lt;/a&gt; to test the examples in the browser
without installing any software.&lt;/p&gt;
&lt;p&gt;For more information, consult the &lt;a href=&#34;https://www.sqlite.org/json1.html&#34;&gt;SQLite JSON documentation&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  <entry xml:base="https://peter-hoffmann.com/feed/atom.xml">
    <title type="text">Tenant Isolation in Snowflake for ML - Operational Patterns</title>
    <id>https://peter-hoffmann.com/2024/tenant-isolation-in-snowflake-for-ml-operational-patterns.html</id>
    <updated>2024-03-12T00:00:00Z</updated>
    <published>2024-03-12T00:00:00Z</published>
    <link href="/2024/tenant-isolation-in-snowflake-for-ml-operational-patterns.html" />
    <author>
      <name>Peter Hoffmann</name>
      <uri>https://peter-hoffmann.com</uri>
    </author>
    <content type="html">&lt;p&gt;Building on the conceptual foundations my previous article &lt;a href=&#34;https://peter-hoffmann.com/2023/tenant-isolation-in-snowflake.html&#34;&gt;Tenant Isolation in
Snowflake&lt;/a&gt;,
this follow-up explores the &lt;strong&gt;practical and operational realities&lt;/strong&gt; teams face
when running &lt;strong&gt;multi-tenant ML workloads&lt;/strong&gt; in production. While isolation
strategies are often discussed at the data-modeling or security level, ML
systems introduce additional complexity: tenant-aware data copies for
experimentation, safe access to customer data in lower environments,
reproducible model testing, and CI/CD-style deployment pipelines spanning dev,
staging, and prod.&lt;/p&gt;
&lt;p&gt;This post focuses on &lt;strong&gt;hands-on patterns&lt;/strong&gt; for operating Snowflake-backed ML
platforms at scale. It covers tenant data duplication strategies, environment
promotion workflows, ML experimentation with real customer data under strict
controls, and operational trade-offs between cost, safety, and velocity. The
goal is to provide concrete guidance for teams who already understand tenant
isolation in theory and now need to make it work reliably for ML-driven
products.&lt;/p&gt;
&lt;h2&gt;Recap: Tenant Isolation — From Concept to Operations&lt;/h2&gt;
&lt;p&gt;Before diving into ML-specific challenges, let’s briefly recap the main tenant
isolation strategies in Snowflake, as discussed in the &lt;a href=&#34;https://peter-hoffmann.com/2023/tenant-isolation-in-snowflake.html&#34;&gt;original
post&lt;/a&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
  &lt;th&gt;Strategy&lt;/th&gt;
  &lt;th&gt;Isolation Strength&lt;/th&gt;
  &lt;th&gt;Characteristics&lt;/th&gt;
  &lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
  &lt;td&gt;&lt;strong&gt;Separate Snowflake Accounts per Tenant&lt;/strong&gt;&lt;/td&gt;
  &lt;td&gt;Strongest&lt;/td&gt;
  &lt;td&gt;Each tenant has its own Snowflake account, users, warehouses, and data. High operational overhead.&lt;/td&gt;
  &lt;td&gt;Large/regulated customers requiring maximum security, compliance, and cost attribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;&lt;strong&gt;Shared Account, Separate Databases per Tenant&lt;/strong&gt;&lt;/td&gt;
  &lt;td&gt;Strong&lt;/td&gt;
  &lt;td&gt;Each tenant gets a dedicated database within a shared account. Easier cost tracking, but some shared blast radius.&lt;/td&gt;
  &lt;td&gt;Most multi-tenant scenarios balancing isolation with operational efficiency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;&lt;strong&gt;Shared Database, Separate Schemas per Tenant&lt;/strong&gt;&lt;/td&gt;
  &lt;td&gt;Moderate&lt;/td&gt;
  &lt;td&gt;Each tenant has a schema in a shared database. Lower overhead, but weaker boundaries and risk of privilege mistakes.&lt;/td&gt;
  &lt;td&gt;Medium-scale deployments with trusted tenants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;&lt;strong&gt;Shared Tables with &lt;code&gt;tenant_id&lt;/code&gt; Column&lt;/strong&gt;&lt;/td&gt;
  &lt;td&gt;Lowest&lt;/td&gt;
  &lt;td&gt;All tenants share tables, isolation enforced by row access policies and application logic. Highest scale, but weakest isolation and higher risk of data leaks.&lt;/td&gt;
  &lt;td&gt;Many small tenants where operational simplicity and scale are prioritized&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Machine learning applications fundamentally differ from traditional software
because their behavior is shaped jointly by code and continuously evolving data,
which necessitates architectures that treat data pipelines, model lifecycle, and
feedback loops as first-class, tightly integrated components rather than
peripheral concerns. So ML workloads introduce new and more dynamic challenges to the architecture:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Production model monitoring and validation&lt;/strong&gt;: In production, teams must continuously monitor, validate, and analyze ML model quality for each tenant. This requires access to up-to-date tenant data, robust logging, and the ability to attribute model performance issues to specific tenants or data segments.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ML training lifecycle&lt;/strong&gt;: The ML lifecycle involves retraining models on fresh tenant data, evaluating new model versions, and promoting them to production. Each stage (training, evaluation, deployment) requires data access to production data, especially when rolling out models incrementally or running A/B tests.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Experimentation in lower environments&lt;/strong&gt;: ML development is highly iterative—data scientists and engineers need to experiment with new models, features, and preprocessing pipelines in dev/staging environments. This often means working with realistic (sometimes real) tenant data, which increases the risk of data leakage and requires careful controls.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data quality checks and remediation&lt;/strong&gt;: Data quality issues are often detected in production, but fixes and validation must be applied in lower environments first. This workflow requires safe, auditable ways to copy, mask, or anonymize tenant data for debugging and remediation without violating isolation boundaries.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reproducibility&lt;/strong&gt;: ML pipelines need consistent, isolated snapshots of data and code.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automation &amp;amp; cost&lt;/strong&gt;: Frequent cloning, cleanup, and environment promotion can drive up costs and require robust automation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compliance&lt;/strong&gt;: ML experiments may touch sensitive data, raising the bar for auditability and blast-radius reduction.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rest of this article explores how to adapt and extend the isolation
patterns to meet the operational realities of multi-tenant ML platforms.&lt;/p&gt;
&lt;h2&gt;Environment Topology for Multi-Tenant ML Systems&lt;/h2&gt;
&lt;p&gt;A typical environment structure maps to the standard &lt;strong&gt;dev, staging, and prod
environments&lt;/strong&gt; and applies extra requiements on how you segment tenants to
achive safety, velocity, and managable operational complexity:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/static/2024/environments.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Development&lt;/strong&gt;: For rapid iteration, experimentation, and debugging. Data
scientists and engineers need flexibility. Guardrails are put in place
(technically and operationally) that no sensitive/customer data must be used in
the development environments to prevent accidental data leaks. All ML development in
this environment must only use anonymized or synthetical data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Staging&lt;/strong&gt;: A pre-production environment for integration testing, model
validation, and data quality checks using production-like data. Staging is where
you catch issues before they impact customers. It should be possible to copy
data from a production tenant to a staging tenant in a controlled way to
validate ML Model behaviour.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Production&lt;/strong&gt;: The live environment serving real tenant workloads, where
safety, auditability, and performance are paramount.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are two main patterns for mapping environments in Snowflake:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Account-per-Environment&lt;/strong&gt;: Each environment (dev, staging, prod) gets its
own Snowflake account. This provides the strongest isolation—no risk of
accidental cross-environment data access, and clear separation of roles,
warehouses, and billing. However, it increases operational overhead and
complicates automation and cross-environment analytics. Tenenat/Data copies
between accounts become more complex and you cannot benefit from Snowflake zero
copy cloning capabilities.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Database-per-Environment&lt;/strong&gt;: All environments live in a single Snowflake account, separated by databases(e.g., &lt;code&gt;myapp_dev&lt;/code&gt;, &lt;code&gt;myapp_staging&lt;/code&gt;, &lt;code&gt;myapp_prod&lt;/code&gt;). This reduces cost and simplifies automation, but increases the risk of accidental data access across environments and requires stricter RBAC and naming conventions.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The challenge multiplies when you add tenants. For each environment, you must decide how to isolate tenants:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Account-per-tenant-per-environment&lt;/strong&gt;: Maximum isolation, but operationally heavy and rarely justified except for the largest, most regulated customers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Database-per-tenant-per-environment&lt;/strong&gt;: A common compromise—each tenant gets a database in each environment. This enables per-tenant backup, restore, and lifecycle management, but can lead to database sprawl as the number of tenants grows.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tenant Data Copy Strategies in Practice&lt;/h2&gt;
&lt;p&gt;Effective data copy strategies are essential for enabling safe ML
experimentation, model retraining, and debugging in multi-tenant Snowflake
environments. The design needs to  balance complexity, speed, cost, and risk—especially
when working with sensitive or large-scale tenant data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zero-Copy Cloning for Tenant-Scoped Datasets&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://docs.snowflake.com/en/user-guide/tables-storage-considerations#label-cloning-tables&#34;&gt;Snowflake’s zero-copy cloning&lt;/a&gt; is a powerful feature for ML workflows. It allows you to instantly create a snapshot of a database, schema, or table for a tenant—without duplicating storage. This is ideal for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Creating isolated dev/staging environments for a tenant&lt;/li&gt;
&lt;li&gt;Running experiments or model training on a consistent data snapshot&lt;/li&gt;
&lt;li&gt;Debugging production issues with a point-in-time copy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Full vs. Partial Tenant Copies (Time-Bounded, Feature-Bounded)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Not all ML use cases require a full copy of tenant data. There are two main ways to reduce data sizes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time-bounded copies&lt;/strong&gt;: Only clone recent data (e.g., last 30 days) to reduce storage and speed up experimentation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Feature-bounded copies&lt;/strong&gt;: Copy only the columns/features needed for a specific ML task, masking or omitting sensitive fields.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cloning of the input data vs. storing the output of the feature pipeline&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When ML development does not include feature development, it might be sufficient
to store the columns/features generated by the data pipeline that is used as the input
for the ML model. A robust metadata management and/or feature store will help to track
data lineage. It is necessary to also put controls in place for derived data to never
leave a tenant context.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost Visibility and Cleanup Automation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Frequent cloning and data copying can quickly lead to unexpected storage costs. Best practices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tag all clones and copies with metadata (tenant, environment, purpose, expiration)&lt;/li&gt;
&lt;li&gt;Automate cleanup of temporary datasets after experiments or model validation&lt;/li&gt;
&lt;li&gt;Monitor storage usage and set alerts for orphaned or stale clones&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;ML Experimentation and Data Cloning&lt;/h2&gt;
&lt;p&gt;Experimentation is at the heart of ML development. An operational pattern for
multi-tenant ML systems is maintaining &lt;strong&gt;point-in-time (PIT) snapshots&lt;/strong&gt; of
production tenant data in staging environments for continuous model validation.
This approach decouples model development from KPI evaluation, ensuring that
performance metrics reflect true model improvements rather than data drift or
quality issues.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Challenge: Data Drift vs. Model Drift&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When validating ML models, you need to distinguish whether performance changes are due to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Model improvements/regressions&lt;/strong&gt;: Changes in model code or training&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data drift&lt;/strong&gt;: Evolving customer behavior or data quality issues&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure changes&lt;/strong&gt;: Schema updates, feature pipeline bugs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without stable reference datasets, KPI monitoring becomes unreliable and debugging is difficult. The Solution is regular PIT Snapshots with Fixed Validation Sets.&lt;/p&gt;
&lt;p&gt;Use Snowflake&#39;s zero-copy cloning to create dated snapshots of production tenant data in staging and keep them as &lt;strong&gt;Immutable Validation Sets&lt;/strong&gt;: Each snapshot remains frozen. New models are evaluated against the same historical data, making results comparable over time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Automated KPI Computation&lt;/strong&gt;: Run standardized validation queries against each snapshot to compute metrics (accuracy, precision, RMSE, etc.) for every model version.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Snapshot Rotation&lt;/strong&gt;: Keep the last N snapshots (e.g., 4-8 weeks) to track model performance trends, then archive or drop older snapshots to control costs.&lt;/p&gt;
&lt;p&gt;This pattern also helps &lt;strong&gt;detect and isolate data quality problems&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If a model&#39;s KPIs drop on a &lt;em&gt;new&lt;/em&gt; snapshot but remain stable on older ones, suspect data drift or pipeline bugs—not model regression.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If KPIs degrade across &lt;em&gt;all&lt;/em&gt; snapshots, the model itself likely has issues.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cost and Storage Management&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Snapshots consume storage incrementally (only changed data), but can accumulate.
Best practices is to set retention policies (e.g., keep 8 weeks, archive to
cheaper storage after 90 days) and use Snowflake&#39;s &lt;code&gt;UNDROP&lt;/code&gt; and Time Travel as
fallbacks instead of keeping every daily snapshot.&lt;/p&gt;
&lt;h2&gt;Deployment Pipelines: From Dev to Prod&lt;/h2&gt;
&lt;p&gt;Robust deployment pipelines are essential for safely and efficiently moving ML
models, features, and data transformations from development to production in
multi-tenant Snowflake environments. These pipelines must account for both code
and data, and support rapid iteration while minimizing risk.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CI/CD Concepts for Data and Models&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The core ML deployment pattern is to treat data pipelines, feature engineering code, and model artifacts as first-class citizens in CI/CD workflows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use version control for all code, configuration, and schema definitions.&lt;/li&gt;
&lt;li&gt;Automate testing of data pipelines and model training in dev/staging before production promotion.&lt;/li&gt;
&lt;li&gt;Integrate model validation and data quality checks as pipeline steps, not afterthoughts.&lt;/li&gt;
&lt;li&gt;Use tools like &lt;a href=&#34;https://mlflow.org/docs/latest/&#34;&gt;MLFlow&lt;/a&gt; to track metadata, KPIs metrics
and artefacts generated during model evaluation. This can include reports, learned model weights and metadata for data snaphots.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Promoting Schemas, Features, and Models Across Environments&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Promotion should be automated and auditable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use migration tools or versioned DDL to apply schema changes consistently across dev, staging, and prod.&lt;/li&gt;
&lt;li&gt;Promote feature definitions and model artifacts only after passing validation on production-like data in staging.&lt;/li&gt;
&lt;li&gt;Tag and track all promoted objects with environment, version, and deployment metadata.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Environment-Specific Snowflake Objects&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Each environment may require different Snowflake resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Warehouses: Size and configure compute resources to match environment needs (e.g., smaller in dev, larger in prod).&lt;/li&gt;
&lt;li&gt;Roles and RBAC: Restrict access in lower environments, and enforce least-privilege in production.&lt;/li&gt;
&lt;li&gt;Tasks and Streams: Automate data ingestion, transformation, and model scoring with environment-specific schedules and parameters.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Rollback Strategies for Multi-Tenant Deployments&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Failures are inevitable—robust rollback is critical:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use zero-copy clones and Time Travel to quickly revert schemas or data to a known good state.&lt;/li&gt;
&lt;li&gt;For model rollouts, support canary or phased deployments (e.g., enable new model for a subset of tenants, monitor KPIs, then expand rollout).&lt;/li&gt;
&lt;li&gt;Maintain previous model versions and feature definitions for rapid rollback if regressions are detected.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By treating data, features, and models as deployable artifacts, and automating
their promotion and rollback, teams can achieve both agility and safety in
multi-tenant ML operations on Snowflake.&lt;/p&gt;
&lt;h2&gt;Operational Trade-Offs and Lessons Learned&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Where Teams Over-Engineer&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Overly granular isolation (e.g., every tenant in its own account/environment) creates a maintenance burden that rarely pays off except for the largest or most regulated customers.&lt;/li&gt;
&lt;li&gt;Excessive manual controls and approval steps slow down experimentation and deployment, leading to bottlenecks and frustrated teams. Data Scientists need access to the data, rather then locking away the production data make sure experiments are run in a controlled/automated fashion.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Where Teams Underestimate Risk&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Under-investing in RBAC, audit logging, and automated data cleanup can lead to costly data leaks or compliance violations.&lt;/li&gt;
&lt;li&gt;Failing to automate environment and tenant provisioning results in configuration drift and inconsistent access boundaries.&lt;/li&gt;
&lt;li&gt;Ignoring cost monitoring for clones, snapshots, and unused resources can cause runaway storage bills.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What to Automate Early&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Environment and tenant provisioning (infrastructure-as-code, templates)&lt;/li&gt;
&lt;li&gt;Data copy/clone tagging, expiration, and cleanup. Expose the capabilities throuhg APIs or CI/CD pipelines&lt;/li&gt;
&lt;li&gt;Schema migrations and model promotion pipelines&lt;/li&gt;
&lt;li&gt;RBAC policy enforcement and regular audits&lt;/li&gt;
&lt;li&gt;Cost monitoring and alerting for storage and compute&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most successful teams revisit their architecture and automation regularly,
adapting as scale and requirements evolve. Start simple, automate aggressively,
and be ready to tighten controls as your platform and customer base grow.&lt;/p&gt;
&lt;h2&gt;Closing Thoughts&lt;/h2&gt;
&lt;p&gt;Tenant isolation in Snowflake for ML is not a one-time architectural decision,
but an ongoing operational discipline. As ML platforms scale, the interplay
between data, code, and tenant boundaries becomes more complex—and more critical
to get right.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaways:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There is no single “best” isolation model; the right approach evolves with your product, customer base, and regulatory landscape.&lt;/li&gt;
&lt;li&gt;Automation, tagging, and regular audits are essential to keep environments, data copies, and access controls manageable at scale.&lt;/li&gt;
&lt;li&gt;ML workloads amplify the risks and operational challenges of multi-tenancy, making reproducibility, rollback, and monitoring non-negotiable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Looking Forward:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Expect the boundaries between data engineering, ML, and platform operations to blur further as teams adopt more advanced automation and governance.&lt;/li&gt;
&lt;li&gt;New Snowflake features (e.g., object tagging, masking policies, data clean rooms) will continue to expand the toolkit for safe, scalable multi-tenant ML.&lt;/li&gt;
&lt;li&gt;The most resilient teams treat tenant isolation as a living process—reviewing, testing, and evolving their patterns as both technology and business needs change.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tenant isolation is a journey, not a destination. By combining strong technical
controls with a culture of continuous improvement, teams can deliver both
agility and safety for ML-driven products on Snowflake.&lt;/p&gt;
</content>
  </entry>
  <entry xml:base="https://peter-hoffmann.com/feed/atom.xml">
    <title type="text">Exploring Mountain Huts with SPARQL and Wikidata</title>
    <id>https://peter-hoffmann.com/2023/exploring-mountain-huts-with-sparql-and-wikidata.html</id>
    <updated>2023-12-23T00:00:00Z</updated>
    <published>2023-12-23T00:00:00Z</published>
    <link href="/2023/exploring-mountain-huts-with-sparql-and-wikidata.html" />
    <author>
      <name>Peter Hoffmann</name>
      <uri>https://peter-hoffmann.com</uri>
    </author>
    <content type="html">&lt;p&gt;For outdoor enthusiasts and avid hikers, searching for mountain huts or shelters is common when planning tours. In this blog post, we will retrieve information about mountain huts around a specific latitude and longitude using the powerful combination of SPARQL and Wikidata.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/static/2023/wikidata-sparql-hut-query.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;h3&gt;Understanding SPARQL&lt;/h3&gt;
&lt;p&gt;SPARQL (SPARQL Protocol and RDF Query Language) is a query language designed for querying data stored in Resource Description Framework (RDF) format. RDF provides a standard way to represent information, making it an ideal choice for querying diverse datasets.&lt;/p&gt;
&lt;h3&gt;Accessing Wikidata:&lt;/h3&gt;
&lt;p&gt;Wikidata, a collaborative knowledge base, hosts a wealth of information on various topics, including geographical features like mountain huts. By running SPARQL queries on the Wikidata platform, we can extract specific details about these huts based on their geographic coordinates.&lt;/p&gt;
&lt;h3&gt;Retrieving Mountain Huts:&lt;/h3&gt;
&lt;p&gt;Let’s look at a SPARQL query to retrieve information about mountain huts around a given latitude and longitude. The following query can be used as a starting point:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;DISTINCT&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?distance&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?place&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?placeLabel&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?lat&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?long&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?elevation&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
  &lt;span class=&#34;k&#34;&gt;SERVICE&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;wikibase&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;around&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
    &lt;span class=&#34;c&#34;&gt;# Items with coordinate location (P625)&lt;/span&gt;
    &lt;span class=&#34;nv&#34;&gt;?place&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;wdt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;P625&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?location&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;
    &lt;span class=&#34;c&#34;&gt;# Circle with a center point in WKT (longitude latitude)&lt;/span&gt;
    &lt;span class=&#34;nn&#34;&gt;bd&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;serviceParam&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;wikibase&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;center&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;quot;Point(8.114444 46.521944)&amp;quot;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;^^&lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;geo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;wktLiteral&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;
    &lt;span class=&#34;c&#34;&gt;# Circle radius in km&lt;/span&gt;
    &lt;span class=&#34;nn&#34;&gt;bd&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;serviceParam&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;wikibase&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;radius&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;quot;10&amp;quot;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;
    &lt;span class=&#34;nn&#34;&gt;bd&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;serviceParam&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;wikibase&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;distance&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?distance&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;
  &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;

  &lt;span class=&#34;nv&#34;&gt;?place&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;P625&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?coordinates&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;
  &lt;span class=&#34;nv&#34;&gt;?coordinates&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;psv&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;P625&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
    &lt;span class=&#34;nn&#34;&gt;wikibase&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;geoLatitude&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?lat&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
    &lt;span class=&#34;nn&#34;&gt;wikibase&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;geoLongitude&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?long&lt;/span&gt;
  &lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;

  &lt;span class=&#34;c&#34;&gt;# Instance of: bivouac shelter (Q879208) or mountain hut (Q182676)&lt;/span&gt;
  &lt;span class=&#34;nv&#34;&gt;?place&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;wdt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;P31&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?subclassOf&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;
  &lt;span class=&#34;k&#34;&gt;VALUES&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?subclassOf&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;wd&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;Q879208&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;wd&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;Q182676&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;

  &lt;span class=&#34;c&#34;&gt;# Labels with fallback languages&lt;/span&gt;
  &lt;span class=&#34;k&#34;&gt;SERVICE&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;wikibase&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;label&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;bd&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;serviceParam&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;wikibase&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;language&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;quot;de,gsw,en,fr,it&amp;quot;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;

  &lt;span class=&#34;k&#34;&gt;OPTIONAL&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?place&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;wdt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;P2044&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?elevation&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;ORDER BY&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;?distance&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Replace the numeric values in the WKT literal Point(longitude latitude) with the desired coordinates. This query retrieves mountain huts within a specified radius (in this example, 10 kilometers) of the given location.&lt;/p&gt;
&lt;p&gt;You can also search around a Wikidata location by using its Wikidata ID (e.g., Q68103 for Interlaken) as a reference:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;wd:Q68103 wdt:P625 ?mainLoc .
# Use the around service
SERVICE wikibase:around {
  # Items with coordinate location (P625)
  ?place wdt:P625 ?location .
  # Circle with ?mainLoc as the center (the coordinate location)
  bd:serviceParam wikibase:center ?mainLoc .
  # Circle radius in km
  bd:serviceParam wikibase:radius &amp;quot;40&amp;quot; .
}
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To try it out, just copy and paste the example into &lt;a href=&#34;https://query.wikidata.org&#34;&gt;https://query.wikidata.org&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can use the Python library SPARQLWrapper to retrieve the results via an API in JSON format:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;from&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;SPARQLWrapper&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SPARQLWrapper&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;JSON&lt;/span&gt;
&lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nn&#34;&gt;json&lt;/span&gt;

&lt;span class=&#34;n&#34;&gt;query&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;SELECT DISTINCT ?distance ?place ?placeLabel ?lat ?long ?elevation WHERE {&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;  SERVICE wikibase:around {&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    ?place wdt:P625 ?location .&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    bd:serviceParam wikibase:center &amp;quot;Point(8.114444 46.521944)&amp;quot;^^geo:wktLiteral .&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    bd:serviceParam wikibase:radius &amp;quot;10&amp;quot; .&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    bd:serviceParam wikibase:distance ?distance .&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;  }&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;  ?place p:P625 ?coordinates .&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;  ?coordinates psv:P625 [&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    wikibase:geoLatitude ?lat ;&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;    wikibase:geoLongitude ?long&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;  ] .&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;  ?place wdt:P31 ?subclassOf .&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;  VALUES ?subclassOf { wd:Q879208 wd:Q182676 } .&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;  SERVICE wikibase:label { bd:serviceParam wikibase:language &amp;quot;de,gsw,en,fr,it&amp;quot; . }&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;  OPTIONAL { ?place wdt:P2044 ?elevation . }&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;}&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;ORDER BY ?distance&lt;/span&gt;
&lt;span class=&#34;s1&#34;&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;

&lt;span class=&#34;n&#34;&gt;user_agent&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;python test&amp;quot;&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;endpoint_url&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;quot;https://query.wikidata.org/sparql&amp;quot;&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;sparql&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SPARQLWrapper&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;endpoint_url&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;agent&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;user_agent&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;sparql&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;setQuery&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;query&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;sparql&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;setReturnFormat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;JSON&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;result&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;sparql&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;queryAndConvert&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;results&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;bindings&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;dumps&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;result&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;indent&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
</content>
  </entry>
  <entry xml:base="https://peter-hoffmann.com/feed/atom.xml">
    <title type="text">Tenant Isolation in Snowflake</title>
    <id>https://peter-hoffmann.com/2023/tenant-isolation-in-snowflake.html</id>
    <updated>2023-10-16T00:00:00Z</updated>
    <published>2023-10-16T00:00:00Z</published>
    <link href="/2023/tenant-isolation-in-snowflake.html" />
    <author>
      <name>Peter Hoffmann</name>
      <uri>https://peter-hoffmann.com</uri>
    </author>
    <content type="html">&lt;p&gt;Multi-tenancy is an architectural approach in which a single software system or
data platform serves multiple independent customers (tenants), while ensuring
that each tenant’s data, workloads, and configurations remain logically or
physically isolated according to defined boundaries. The goal of multi-tenancy
is to maximize resource efficiency and operational scalability while preserving
security, performance predictability, and administrative separation, with
isolation implemented at one or more layers such as infrastructure, accounts,
databases, schemas, or rows within shared tables.&lt;/p&gt;
&lt;p&gt;There is no single best architecture and it is always a balance between
competing goals, architectural choices, customer size, and the total number of
customers being served. This post walks through the main tenant isolation
strategies in Snowflake—from fully separate accounts down to logical isolation
with a &lt;code&gt;tenant_id&lt;/code&gt; column—covering pros, cons, best practices, and operational
considerations.&lt;/p&gt;
&lt;p&gt;This article focuses primarily on tenant isolation from a data storage and data
access perspective, examining how tenants can be separated using accounts,
databases, schemas, or shared tables. It is important to note, however, that
Snowflake introduces a separate and equally important dimension through its
compute layer, where virtual warehouses provide independent scaling, workload
isolation, and performance control. While storage-level isolation defines how
data is organized and secured, compute-level isolation plays a critical role in
managing concurrency, cost, and noisy-neighbor effects.&lt;/p&gt;
&lt;h2&gt;What Do We Mean by Tenant Isolation?&lt;/h2&gt;
&lt;p&gt;Tenant isolation typically aims to achieve some combination of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Security isolation&lt;/strong&gt; – preventing data leakage across tenants&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance isolation&lt;/strong&gt; – avoiding noisy-neighbor effects&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational independence&lt;/strong&gt; – deploying changes or fixes per tenant&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost visibility&lt;/strong&gt; – attributing usage to tenants&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scalability&lt;/strong&gt; – onboarding and offboarding tenants efficiently&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Snowflake’s architecture (separate storage and compute, strong RBAC, and secure
sharing) allows you to choose isolation at multiple layers.&lt;/p&gt;
&lt;h2&gt;Option 1: Separate Snowflake Accounts per Tenant&lt;/h2&gt;
&lt;p&gt;Tenant isolation using &lt;strong&gt;separate Snowflake accounts&lt;/strong&gt; is best suited for large
enterprise customers and for scenarios with strict compliance or regulatory
requirements where strong isolation is mandatory. It is also appropriate for
tenants with highly variable or unpredictable workloads, as well as for “bring
your own Snowflake” models where customers operate within their own Snowflake
environments.&lt;/p&gt;
&lt;h3&gt;Description&lt;/h3&gt;
&lt;p&gt;Each tenant gets its own Snowflake account. Data, users, warehouses, and
security policies are completely isolated.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;Tenant A → Snowflake Account A  
Tenant B → Snowflake Account B
&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Best Practices&lt;/h3&gt;
&lt;p&gt;Snowflake Organizations should be used to centrally manage and govern multiple
Snowflake accounts, providing a unified view for administration, security, and
billing across tenants. This helps reduce operational overhead while maintaining
strong account-level isolation.&lt;/p&gt;
&lt;p&gt;Account and tenant provisioning should be fully automated using
infrastructure-as-code tools such as Terraform or Snowflake’s APIs. Automation
ensures consistency, reduces manual errors, and makes onboarding and offboarding
tenants predictable and repeatable at scale.&lt;/p&gt;
&lt;p&gt;Role definitions, warehouse configurations, and database naming conventions
should be standardized across all tenants. Consistent patterns simplify access
management, monitoring, and troubleshooting, and they make it significantly
easier to apply changes or enforce policies uniformly.&lt;/p&gt;
&lt;p&gt;When cross-tenant or centralized analytics is required, Snowflake’s secure data
sharing capabilities should be used instead of copying data between accounts.
Secure sharing enables controlled access to tenant data while preserving
isolation and minimizing data duplication.&lt;/p&gt;
&lt;h3&gt;Pros&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Strongest isolation&lt;/strong&gt; (security and blast radius)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clear cost attribution&lt;/strong&gt; per tenant&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Independent upgrades and experiments&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Regulatory friendly&lt;/strong&gt; (data residency, compliance)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Cons&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;High operational overhead&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Account provisioning&lt;/li&gt;
&lt;li&gt;User and role management&lt;/li&gt;
&lt;li&gt;Monitoring and alerting per account&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Harder cross-tenant analytics&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More complex CI/CD&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Potentially higher cost&lt;/strong&gt; for small tenants&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Option 2: Shared Account, Separate Databases per Tenant&lt;/h2&gt;
&lt;p&gt;A shared account with &lt;strong&gt;separate databases per tenant&lt;/strong&gt; is well suited for a medium
number of tenants with moderate compliance requirements. This approach is
particularly useful when per-tenant backup, restore, or data lifecycle
operations need to be handled independently without the overhead of multiple
accounts.&lt;/p&gt;
&lt;h3&gt;Description&lt;/h3&gt;
&lt;p&gt;All tenants live in one Snowflake account, but each tenant gets its own database.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;ACCOUNT
 ├── TENANT_A_DB
 ├── TENANT_B_DB
&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Best Practices&lt;/h3&gt;
&lt;p&gt;Each tenant should be assigned its own database while maintaining an identical
schema structure across all tenant databases. This consistency simplifies
development, testing, and operations, and allows changes to be applied
predictably across tenants.&lt;/p&gt;
&lt;p&gt;Database roles should be used to control tenant access at the database level.
Leveraging database roles provides clear separation of privileges, reduces the
risk of misconfiguration, and aligns well with Snowflake’s role-based access
control model.&lt;/p&gt;
&lt;p&gt;Databases should be tagged with metadata such as tenant, environment, and
cost_center to support cost tracking, governance, and operational visibility.
Tags make it easier to analyze usage, implement chargeback or showback models,
and apply policies consistently.&lt;/p&gt;
&lt;p&gt;Schema migrations should be fully automated across all tenant databases using
standardized deployment pipelines or migration tools. Automation ensures schema
changes are applied reliably and uniformly, minimizing drift and reducing the
operational burden as the number of tenants grows.&lt;/p&gt;
&lt;h3&gt;Pros&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Strong logical isolation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Easier to manage than multiple accounts&lt;/li&gt;
&lt;li&gt;Database-level privileges are simple and explicit&lt;/li&gt;
&lt;li&gt;Easier cost tracking using database tags and query history&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Cons&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Still some &lt;strong&gt;shared blast radius&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Schema migrations must be coordinated across databases&lt;/li&gt;
&lt;li&gt;Large numbers of tenants can lead to database sprawl&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Option 3: Shared Database, Separate Schemas per Tenant&lt;/h2&gt;
&lt;p&gt;Tenant isolation using a &lt;strong&gt;share databases with schema separation per Tenant&lt;/strong&gt; within
a single Snowflake account is well suited for smaller tenants and internal
multi-team environments where strong logical separation is needed without the
overhead of multiple accounts. It is also a good fit for early-stage SaaS
platforms that want to balance isolation, simplicity, and operational efficiency
while they scale.&lt;/p&gt;
&lt;h3&gt;Description&lt;/h3&gt;
&lt;p&gt;A single database hosts multiple schemas, one per tenant.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;DATABASE
 ├── TENANT_A_SCHEMA
 ├── TENANT_B_SCHEMA
&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;Best Practices&lt;/h3&gt;
&lt;p&gt;Strict role-to-schema mappings should be enforced so that each role has access
only to the schemas it is explicitly responsible for. This minimizes the risk of
accidental data exposure and makes access boundaries clear and auditable.&lt;/p&gt;
&lt;p&gt;Cross-schema references should be avoided wherever possible, as they weaken
isolation guarantees and make it harder to reason about data ownership and
access paths. Keeping schemas self-contained improves security, maintainability,
and portability.&lt;/p&gt;
&lt;p&gt;Consistent naming conventions for schemas, roles, and objects should be used
across all tenants. Standardization simplifies automation, monitoring, and
troubleshooting, and reduces cognitive overhead for operators and developers.&lt;/p&gt;
&lt;p&gt;Grants and permissions should be periodically audited to ensure they still
reflect intended access patterns. Regular reviews help detect privilege creep,
misconfigurations, and potential security gaps before they become issues.&lt;/p&gt;
&lt;h3&gt;Pros&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Fewer objects to manage than databases&lt;/li&gt;
&lt;li&gt;Faster onboarding of new tenants&lt;/li&gt;
&lt;li&gt;Simple to share common reference tables&lt;/li&gt;
&lt;li&gt;Lower operational overhead&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Cons&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Weaker isolation than databases&lt;/li&gt;
&lt;li&gt;Schema-level privilege mistakes can cause data exposure&lt;/li&gt;
&lt;li&gt;Schema explosion with many tenants&lt;/li&gt;
&lt;li&gt;Harder to enforce per-tenant performance limits&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Option 4: Shared Tables with &lt;code&gt;tenant_id&lt;/code&gt; Column&lt;/h2&gt;
&lt;p&gt;This approach is well suited for environments with a large number of small
tenants and primarily read-heavy analytics workloads, where sharing
infrastructure provides significant efficiency gains. It allows the platform to
scale to many customers without excessive operational overhead.&lt;/p&gt;
&lt;p&gt;Because isolation is largely enforced logically, it works best when strong
application-level controls are in place to prevent cross-tenant access and
enforce correct query patterns. It is also an attractive option for
cost-sensitive environments, as shared storage and compute help minimize overall
platform expenses.&lt;/p&gt;
&lt;h3&gt;Description&lt;/h3&gt;
&lt;p&gt;All tenants share the same tables, distinguished by a &lt;code&gt;tenant_id&lt;/code&gt; column.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;orders&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tenant_id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;tenant_123&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Isolation is enforced through application logic and Snowflake features such as row access policies.&lt;/p&gt;
&lt;h3&gt;Best Practices&lt;/h3&gt;
&lt;p&gt;Access to shared tables should be enforced using row access policies to ensure
that each tenant can only see its own data, regardless of how queries are
written. This provides a critical safety net and reduces reliance on application
logic alone for isolation.&lt;/p&gt;
&lt;p&gt;The tenant_id should always be included in primary keys and used consistently in
table design, as this makes tenant boundaries explicit and prevents accidental
key collisions. Including tenant_id also enables more efficient pruning and
predictable query behavior.&lt;/p&gt;
&lt;p&gt;Clustering tables by tenant_id should be considered, especially when tenants
frequently query their own data in isolation. Proper clustering can
significantly improve query performance and reduce unnecessary data scanning in
large shared tables.&lt;/p&gt;
&lt;p&gt;Automated tests should be added to validate that all queries include the
appropriate tenant filters and that row access policies are correctly applied.
Testing helps catch regressions early and prevents subtle mistakes from leading
to cross-tenant data exposure.&lt;/p&gt;
&lt;p&gt;Finally, query patterns should be continuously monitored to detect any signs of
cross-tenant access or anomalous behavior. Regular analysis of query and access
logs helps identify misconfigurations, misuse, or potential security issues
before they escalate.&lt;/p&gt;
&lt;h3&gt;Pros&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lowest operational overhead&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Easy schema evolution&lt;/li&gt;
&lt;li&gt;Excellent for analytics across tenants&lt;/li&gt;
&lt;li&gt;Efficient storage usage&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Cons&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Weakest isolation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Higher risk of data leaks&lt;/li&gt;
&lt;li&gt;Requires disciplined query patterns&lt;/li&gt;
&lt;li&gt;Harder to delete or export a single tenant’s data&lt;/li&gt;
&lt;li&gt;Performance contention between tenants&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Operational Considerations Across All Models&lt;/h2&gt;
&lt;h3&gt;1. Cost Management&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;resource monitors&lt;/strong&gt; at warehouse or account level&lt;/li&gt;
&lt;li&gt;Tag warehouses, databases, or queries with tenant metadata&lt;/li&gt;
&lt;li&gt;Periodically review &lt;code&gt;QUERY_HISTORY&lt;/code&gt; and &lt;code&gt;WAREHOUSE_METERING_HISTORY&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. Performance Isolation&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Separate warehouses for:&lt;ul&gt;
&lt;li&gt;ETL&lt;/li&gt;
&lt;li&gt;Customer queries&lt;/li&gt;
&lt;li&gt;Internal analytics&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Size warehouses based on tenant tier&lt;/li&gt;
&lt;li&gt;Consider multi-cluster warehouses for concurrency spikes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Security and Auditing&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Follow least-privilege RBAC&lt;/li&gt;
&lt;li&gt;Regularly audit grants (&lt;code&gt;SHOW GRANTS&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Enable access history and query logging&lt;/li&gt;
&lt;li&gt;Consider masking policies for PII&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. Data Lifecycle Management&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Define per-tenant retention and purge policies&lt;/li&gt;
&lt;li&gt;Automate tenant offboarding&lt;/li&gt;
&lt;li&gt;Plan for per-tenant export or deletion early&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. CI/CD and Schema Changes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Treat schemas as code&lt;/li&gt;
&lt;li&gt;Use migration tools or versioned DDL&lt;/li&gt;
&lt;li&gt;Test changes against multiple tenants&lt;/li&gt;
&lt;li&gt;Avoid manual schema drift&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Choosing the Right Model&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
  &lt;th&gt;Requirement&lt;/th&gt;
  &lt;th&gt;Recommended Approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
  &lt;td&gt;Maximum isolation&lt;/td&gt;
  &lt;td&gt;Separate accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;Strong isolation, fewer ops&lt;/td&gt;
  &lt;td&gt;Database per tenant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;Moderate isolation&lt;/td&gt;
  &lt;td&gt;Schema per tenant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;High scale, low cost&lt;/td&gt;
  &lt;td&gt;Shared tables with &lt;code&gt;tenant_id&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In practice, you often implement &lt;strong&gt;hybrid approaches&lt;/strong&gt; where one or more
customers are grouped within a single Snowflake account, while each customer is
isolated using a dedicated database. This model strikes a balance between strong
logical isolation and manageable operational overhead, allowing teams to scale
without fully committing to an account-per-tenant strategy.&lt;/p&gt;
&lt;p&gt;This pattern is often aligned with a &lt;strong&gt;cell-based architecture&lt;/strong&gt;, where each account
represents a cell that hosts a bounded set of customers with similar
characteristics, such as region, compliance profile, or service tier. Within a
cell, customers are isolated at the database level, while additional cells can
be added over time to limit blast radius, control growth, and support horizontal
scaling. This approach enables predictable operations, clearer governance
boundaries, and a gradual path toward stronger isolation for customers that
outgrow their current cell.&lt;/p&gt;
&lt;h2&gt;Additional Documentation&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Snowflake Architecture Overview&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Foundational reading for understanding why different isolation models behave the way they do.&lt;/em&gt;&lt;br /&gt;
&lt;a href=&#34;https://docs.snowflake.com/en/user-guide/intro-key-concepts&#34;&gt;https://docs.snowflake.com/en/user-guide/intro-key-concepts&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Best Practices for Designing Snowflake Databases&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Covers logical separation, schema organization, and object management.&lt;/em&gt;&lt;br /&gt;
&lt;a href=&#34;https://docs.snowflake.com/en/user-guide/db-design&#34;&gt;https://docs.snowflake.com/en/user-guide/db-design&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Snowflake Multi-Account Strategy (Organizations)&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Essential for account-per-tenant designs.&lt;/em&gt;&lt;br /&gt;
&lt;a href=&#34;https://docs.snowflake.com/en/user-guide/organizations&#34;&gt;https://docs.snowflake.com/en/user-guide/organizations&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Building SaaS Applications on Snowflake (Whitepaper)&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;One of the best high-level discussions of tenant isolation patterns.&lt;/em&gt;&lt;br /&gt;
&lt;a href=&#34;https://www.snowflake.com/resource/building-saas-applications-on-snowflake/&#34;&gt;https://www.snowflake.com/resource/building-saas-applications-on-snowflake/&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Microsoft: Multi-Tenant SaaS Database Patterns&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Excellent conceptual grounding; applies cleanly to Snowflake.&lt;/em&gt;&lt;br /&gt;
&lt;a href=&#34;https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/overview&#34;&gt;https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/overview&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AWS SaaS Tenant Isolation Strategies&lt;/strong&gt;&lt;br /&gt;
&lt;em&gt;Useful framework for evaluating isolation strength, regardless of platform.&lt;/em&gt;&lt;br /&gt;
&lt;a href=&#34;https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/tenant-isolation.html&#34;&gt;https://docs.aws.amazon.com/wellarchitected/latest/saas-lens/tenant-isolation.html&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry xml:base="https://peter-hoffmann.com/feed/atom.xml">
    <title type="text">Blue Yonder at PyCon.DE 2023</title>
    <id>https://peter-hoffmann.com/2023/blueyonder-at-pyconde-2023.html</id>
    <updated>2023-04-25T00:00:00Z</updated>
    <published>2023-04-25T00:00:00Z</published>
    <link href="/2023/blueyonder-at-pyconde-2023.html" />
    <author>
      <name>Peter Hoffmann</name>
      <uri>https://peter-hoffmann.com</uri>
    </author>
    <content type="html">&lt;p&gt;&lt;strong&gt;Blue Yonder History&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It has now been 10 years since &lt;a href=&#34;https://blueyonder.com&#34;&gt;Blue Yonder&lt;/a&gt; started its first sponsorship of a Python conference at EuroPython in Florence. Since then, we have been sponsoring or organizing at least one Python event per year. For me, this has always been part of my mission: to convince leadership and fellow team leads that participating in the open-source community benefits employee development and overall corporate culture. Young engineers learn to represent the company and connect with other open-source developers.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/static/2023/pycon-booth.jpg&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;This year, PyCon.DE was hosted in Berlin. With 1,500 attendees, the conference has grown tremendously over the past years. The Berlin Congress Center is, of course, a very professional venue, and the organizing committee did an excellent job running a smooth conference overall. Still, I hope that &lt;a href=&#34;https://pycon.de&#34;&gt;PyCon.de&lt;/a&gt; will be hosted in another city next year (maybe even in Switzerland or Austria). Leipzig, Frankfurt, Hamburg, Basel, Bern, or Wien would be great locations for 2024.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cyclic Boosting&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The main topic of this year&#39;s Blue Yonder conference booth was the open-sourcing of &lt;a href=&#34;https://cyclicboosting.org&#34;&gt;Cyclic Boosting&lt;/a&gt;. Cyclic Boosting has been Blue Yonder’s core ML algorithm for many years. Felix Wick gave a
talk about exploring the power of &lt;a href=&#34;https://pretalx.com/pyconde-pydata-berlin-2023/talk/MYARJG/&#34;&gt;Cyclic Boosting: a pure-Python, explainable,
and efficient ML
method&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We also hosted a small Kaggle &lt;a href=&#34;https://www.kaggle.com/competitions/blueyonder-pyconpydata-2023/overview&#34;&gt;ML/Retail
Challenge&lt;/a&gt;
where the open-source community can apply their ML algorithms to a typical
retail problem and benchmark them against a baseline Cyclic Boosting model.&lt;/p&gt;
&lt;p&gt;The challenge is to accurately forecast demand for 300 retail products
across 20 retail stores. Accurate demand forecasting is crucial for retailers
because it enables informed decisions regarding inventory
management, pricing strategies, and sales projections, all of which can
significantly impact the bottom line. In short, demand forecasting is
essential for retailers aiming to optimize operations and maximize profits
to stay competitive in a crowded retail landscape.&lt;/p&gt;
&lt;p&gt;It was quite fun to discuss solutions and technical approaches at our booth,
and people were highly engaged, trying to beat Felix’s reference implementation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Notable PyCon.DE talks&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://2023.pycon.de/program/TP7ABB/&#34;&gt;Wald: A Modern and Sustainable Analytics Stack&lt;/a&gt; from Florian Wilhelm.&lt;/p&gt;
&lt;p&gt;The name WALD stack comes from the four technologies it is composed of, i.e., a
cloud data warehouse such as Snowflake or Google BigQuery, the open-source
data integration engine Airbyte, the open-source full-stack BI platform
Lightdash, and the open-source data transformation tool dbt.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://pretalx.com/pyconde-pydata-berlin-2023/talk/MQHTHY/&#34;&gt;Pragmatic ways of using Rust in your data project&lt;/a&gt; from Christopher Prohm&lt;/p&gt;
&lt;p&gt;Writing efficient data pipelines in Python can be tricky. The standard
recommendation is to use vectorized functions implemented in NumPy, pandas, or
similar libraries. However, what do you do when the processing task does not fit these
libraries? Using plain Python can result in poor performance,
particularly when handling large datasets.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://pretalx.com/pyconde-pydata-berlin-2023/talk/9Q38VT/&#34;&gt;Actionable Machine Learning in the Browser with PyScript&lt;/a&gt; from Valero Maggio&lt;/p&gt;
&lt;p&gt;PyScript brings the full PyData stack to the browser, opening up
unprecedented use cases for interactive, data-intensive applications. In this
scenario, the web browser becomes a ubiquitous computing platform, operating
within a (nearly) zero-installation and serverless environment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hiring&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We are hiring new talent for our AI/ML teams at Blue Yonder. If you are
interested in one of the following positions—Senior Machine Learning Engineer, Senior Data Engineer, or Full-Stack Developer—just reach out to me.&lt;/p&gt;
</content>
  </entry>
</feed>
