<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:base="https://blog.eleni.co">
    <title>Eleni&#39;s Garden</title>
    <link href="https://blog.eleni.co/feed.xml" rel="self" >
    <link href="https://blog.eleni.co" >
    <updated>2025-08-31T12:14:38Z</updated>
    <id>https://blog.eleni.co</id>
        <entry>
            <title>
                Upload the media to your S3 bucket
                
            </title>
            <updated>2024-08-09T12:12:12.290Z</updated>
            <id>https://blog.eleni.co/garden/obsidian-public-media/upload-the-media-to-your-s3-bucket/</id>
            <content type="html">
                &lt;h1 id=&quot;upload-the-media-to-your-s3-bucket&quot; tabindex=&quot;-1&quot;&gt;Upload the media to your S3 bucket&lt;/h1&gt;
&lt;p&gt;Part of: &lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/hosting-obsidian-s3-media-publicly/&quot;&gt;Hosting Obsidian S3 media publicly&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Make sure you have followed the previous steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/obsidian-public-media/setup-s3-bucket-for-use-with-cloudfront/&quot;&gt;Setup S3 bucket for use with Cloudfront&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/obsidian-public-media/setup-custom-aws-cloud-front-cname/&quot;&gt;Setup custom AWS CloudFront CNAME&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/obsidian-public-media/setup-an-iam-user-to-manage-an-s3-bucket/&quot;&gt;Setup an IAM user to manage an S3 bucket&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;use-the-obsidian-plugin&quot; tabindex=&quot;-1&quot;&gt;Use the Obsidian Plugin&lt;/h2&gt;
&lt;p&gt;You can use the excellent &lt;a href=&quot;https://github.com/jvsteiner/s3-image-uploader&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;S3 Image Uploader plugin&lt;/a&gt; to automatically upload images to your S3 bucket.&lt;/p&gt;
&lt;h3 id=&quot;installation&quot; tabindex=&quot;-1&quot;&gt;Installation&lt;/h3&gt;
&lt;p&gt;You can find the plugin in the list of Obsidian community plugins.&lt;/p&gt;
&lt;h3 id=&quot;configure-credentials-for-upload&quot; tabindex=&quot;-1&quot;&gt;Configure credentials for upload&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Provide the &lt;code&gt;Access Key ID&lt;/code&gt; and &lt;code&gt;Secret Access Key&lt;/code&gt; from the IAM you&#39;ve previously created.&lt;/li&gt;
&lt;li&gt;Provide your bucket&#39;s region (e.g. &lt;code&gt;eu-central-1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Provide your bucket&#39;s name (e.g. &lt;code&gt;assets.example.com&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;configure-cloudfront-url&quot; tabindex=&quot;-1&quot;&gt;Configure Cloudfront URL&lt;/h3&gt;
&lt;p&gt;The final step is to configure the domain we previously setup:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Enable &lt;code&gt;Use custom image URL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;Custom Image URL&lt;/code&gt; to the domain you previously configured with Cloudfront (e.g. &lt;code&gt;assets.example.com&lt;/code&gt;). For example for this blog:&lt;br /&gt;
&lt;img src=&quot;https://blog-assets.eleni.co/f1d7d55a5eb8f3cb59fcdc9790afd84b.png&quot; alt=&quot;image&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;manual-alternative-manage-s3-bucket-with-cyberduck&quot; tabindex=&quot;-1&quot;&gt;Manual Alternative: Manage S3 bucket with Cyberduck 🦆&lt;/h2&gt;
&lt;p&gt;You can also manually manage S3 &amp;quot;files&amp;quot; (objects) using &lt;a href=&quot;https://cyberduck.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Cyberduck&lt;/a&gt;.&lt;br /&gt;
There&#39;s quite a bit of &lt;a href=&quot;https://docs.cyberduck.io/protocols/s3/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;documentation&lt;/a&gt;, but there are suggestions I didn&#39;t like (e.g. user with full S3 permissions), and some parts that didn&#39;t work (e.g. bucket name in the server url).&lt;/p&gt;
&lt;p&gt;Here&#39;s what did work for me:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/obsidian-public-media/setup-an-iam-user-to-manage-an-s3-bucket/&quot;&gt;Setup an IAM user to manage an S3 bucket&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Set up an &lt;code&gt;S3 (HTTPS)&lt;/code&gt; bookmark in Cyberduck:
&lt;ol&gt;
&lt;li&gt;Provide the &lt;code&gt;Access Key ID&lt;/code&gt; and &lt;code&gt;Secret Access Key&lt;/code&gt;.&lt;br /&gt;
PS: if you forgot to write these down, you can recreate them.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;Server&lt;/code&gt; to &lt;code&gt;s3.amazonaws.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;More Options -&amp;gt; Path&lt;/code&gt; to the name of your bucket, e.g. &lt;code&gt;/assets.example.com&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;That&#39;s it 🍰&lt;/li&gt;
&lt;/ol&gt;

            </content>
            <link href="https://blog.eleni.co/garden/obsidian-public-media/upload-the-media-to-your-s3-bucket/" >
        </entry>
        <entry>
            <title>
                Setup custom AWS CloudFront CNAME
                
            </title>
            <updated>2024-08-09T12:05:56.299Z</updated>
            <id>https://blog.eleni.co/garden/obsidian-public-media/setup-custom-aws-cloud-front-cname/</id>
            <content type="html">
                &lt;h1 id=&quot;setup-custom-aws-cloud-front-cname&quot; tabindex=&quot;-1&quot;&gt;Setup custom AWS CloudFront CNAME&lt;/h1&gt;
&lt;p&gt;Part of: &lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/hosting-obsidian-s3-media-publicly/&quot;&gt;Hosting Obsidian S3 media publicly&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;AWS CloudFront distributions provide a random subdomain (e.g. &lt;code&gt;a2cc2124adda.cloudfront.net&lt;/code&gt;), which comes with an SSL certificate.&lt;br /&gt;
If we want to use a custom domain name, we have to also setup an SSL certificate.&lt;/p&gt;
&lt;p&gt;Here&#39;s the summary to save the trips to the various guides scattered around AWS docs.&lt;/p&gt;
&lt;p&gt;Assuming we want to point an AWS CloudFront to use &lt;code&gt;assets.example.com&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;On &lt;a href=&quot;https://us-east-1.console.aws.amazon.com/acm/home?region=us-east-1#/welcome&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;AWS Certificate Manager (ACM)&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Create a new public certificate&lt;/li&gt;
&lt;li&gt;NB: The region &lt;strong&gt;must be&lt;/strong&gt; US-East N. Virginia!&lt;/li&gt;
&lt;li&gt;You can also get it from somewhere else and import it&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;On the DNS provider:
&lt;ul&gt;
&lt;li&gt;Add verification CNAME&lt;/li&gt;
&lt;li&gt;Add CNAME and pointing from domain to distribution domain, e.g. &lt;code&gt;assets.example.com -&amp;gt; a2cc2124adda.cloudfront.net&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Ensure it all works:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;&amp;gt; dig assets.example.com
# ....
assets.example.com.	13104	IN	CNAME	a2cc2124adda.cloudfront.net.
# a2cc2124adda.cloudfront.net. 54 IN	A	3.162.78.124
# ....
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;On &lt;a href=&quot;https://us-east-1.console.aws.amazon.com/cloudfront/v4/home#/distributions&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;AWS CloudFront&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;Find the distribution&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Settings -&amp;gt; Edit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set up the CNAME from the previous step, i.e. &lt;code&gt;assets.example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set up the certificate created above&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Done, your images should now be accessible through your prefered domain! 🍰&lt;/li&gt;
&lt;/ol&gt;

            </content>
            <link href="https://blog.eleni.co/garden/obsidian-public-media/setup-custom-aws-cloud-front-cname/" >
        </entry>
        <entry>
            <title>
                Setup an IAM user to manage an S3 bucket
                
            </title>
            <updated>2024-08-09T12:05:56.285Z</updated>
            <id>https://blog.eleni.co/garden/obsidian-public-media/setup-an-iam-user-to-manage-an-s3-bucket/</id>
            <content type="html">
                &lt;h1 id=&quot;setup-an-iam-user-to-manage-an-s3-bucket&quot; tabindex=&quot;-1&quot;&gt;Setup an IAM user to manage an S3 bucket&lt;/h1&gt;
&lt;p&gt;Part of: &lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/hosting-obsidian-s3-media-publicly/&quot;&gt;Hosting Obsidian S3 media publicly&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;We can create an IAM user that only has permissions to manage a desired bucket, e.g. &lt;code&gt;S3-assets-manager&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The most basic permissions would be to just list and manage objects in the entire bucket:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &amp;quot;Version&amp;quot;: &amp;quot;2012-10-17&amp;quot;,
    &amp;quot;Statement&amp;quot;: [
        {
            &amp;quot;Sid&amp;quot;: &amp;quot;VisualEditor1&amp;quot;,
            &amp;quot;Effect&amp;quot;: &amp;quot;Allow&amp;quot;,
            &amp;quot;Action&amp;quot;: [
                &amp;quot;s3:List*&amp;quot;,
                &amp;quot;s3:GetObject&amp;quot;,
                &amp;quot;s3:PutObject&amp;quot;,
                &amp;quot;s3:DeleteObject&amp;quot;
            ],
            &amp;quot;Resource&amp;quot;: [
                &amp;quot;arn:aws:s3:::assets.example.com&amp;quot;,
                &amp;quot;arn:aws:s3:::assets.example.com/*&amp;quot;
            ]
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!info] More Permissions&lt;br /&gt;
Permission to list buckets may also be useful in some cases (e.g. if needed for an integration that&#39;s bucket-name agnostic).&lt;/p&gt;
&lt;/blockquote&gt;

            </content>
            <link href="https://blog.eleni.co/garden/obsidian-public-media/setup-an-iam-user-to-manage-an-s3-bucket/" >
        </entry>
        <entry>
            <title>
                Setup S3 bucket for use with Cloudfront
                
            </title>
            <updated>2024-08-09T12:05:56.280Z</updated>
            <id>https://blog.eleni.co/garden/obsidian-public-media/setup-s3-bucket-for-use-with-cloudfront/</id>
            <content type="html">
                &lt;h1 id=&quot;setup-s3-bucket-for-use-with-cloudfront&quot; tabindex=&quot;-1&quot;&gt;Setup S3 bucket for use with Cloudfront&lt;/h1&gt;
&lt;p&gt;Part of: &lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/hosting-obsidian-s3-media-publicly/&quot;&gt;Hosting Obsidian S3 media publicly&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create an S3 bucket&lt;br /&gt;
e.g. &lt;code&gt;assets.example.com&lt;/code&gt;
&lt;blockquote&gt;
&lt;p&gt;[!info] Public or Private?&lt;br /&gt;
Unless you have a specific public use in mind, you can &lt;strong&gt;keep this bucket private&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;Create a Cloudfront distribution
&lt;ul&gt;
&lt;li&gt;As &lt;code&gt;Origin domain&lt;/code&gt; select your S3 bucket&lt;/li&gt;
&lt;li&gt;You can leave &lt;code&gt;Custom SSL Certificate&lt;/code&gt; empty to start with and set it up later (see &lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/obsidian-public-media/setup-custom-aws-cloud-front-cname/&quot;&gt;Setup custom AWS CloudFront CNAME&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Done 🍰&lt;/li&gt;
&lt;/ol&gt;

            </content>
            <link href="https://blog.eleni.co/garden/obsidian-public-media/setup-s3-bucket-for-use-with-cloudfront/" >
        </entry>
        <entry>
            <title>
                Welcome
                
            </title>
            <updated>2025-06-01T10:34:12.731Z</updated>
            <id>https://blog.eleni.co/</id>
            <content type="html">
                &lt;h1 id=&quot;ello&quot; tabindex=&quot;-1&quot;&gt;👋 &#39;ello!&lt;/h1&gt;
&lt;p&gt;Thanks for stopping by 🙂&lt;br /&gt;
I&#39;m &lt;code&gt;Eleni Lixourioti&lt;/code&gt; (&lt;a href=&quot;https://namedrop.io/elenilixourioti&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;🔊&lt;/a&gt;) , welcome to my &amp;quot;digital garden&amp;quot;!&lt;/p&gt;
&lt;h2 id=&quot;find-me-on&quot; tabindex=&quot;-1&quot;&gt;Find me on&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Geekfish&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;🤖 Github&lt;/a&gt; |  &lt;a href=&quot;https://www.linkedin.com/in/%F0%9F%A4%96-eleni-lixourioti-07b88719/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;💼 LinkedIn&lt;/a&gt; | &lt;a href=&quot;https://bsky.app/profile/eleni.co&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;🦋 Bluesky&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://www.instagram.com/geekfish/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;🤳 Instagram&lt;/a&gt; | &lt;a href=&quot;https://photos.eleni.co/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;📷 Adobe Portfolio&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;...or by good old &lt;a href=&quot;mailto:eleni.co@eleni.mozmail.com&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;email&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;pinned&quot; tabindex=&quot;-1&quot;&gt;Pinned&lt;/h2&gt;
&lt;ul class=&quot;block-language-dataview&quot;&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/microbit-project-3-plants-have-feelings-too/&quot;&gt;Microbit Project 3 - Plants have feelings too&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;recently-added&quot; tabindex=&quot;-1&quot;&gt;Recently added&lt;/h2&gt;
&lt;ul class=&quot;block-language-dataview&quot;&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/needle-alignment-tip-singer-quantum-stylist-9960/&quot;&gt;Needle Alignment Tip - Singer Quantum Stylist 9960&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/phoenix-plug-basic-auth-config-at-runtime/&quot;&gt;Phoenix Plug.BasicAuth config at runtime&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/karvonen-hr-training-zones/&quot;&gt;Karvonen HR training zones&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/obsidian-public-media/upload-the-media-to-your-s3-bucket/&quot;&gt;Upload the media to your S3 bucket&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/obsidian-public-media/setup-an-iam-user-to-manage-an-s3-bucket/&quot;&gt;Setup an IAM user to manage an S3 bucket&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/top-tags/&quot;&gt;Top tags&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/obsidian-public-media/setup-s3-bucket-for-use-with-cloudfront/&quot;&gt;Setup S3 bucket for use with Cloudfront&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/hosting-obsidian-s3-media-publicly/&quot;&gt;Hosting Obsidian S3 media publicly&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/the-time-travelling-islands/&quot;&gt;The Time-Travelling Islands&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/obsidian-public-media/setup-custom-aws-cloud-front-cname/&quot;&gt;Setup custom AWS CloudFront CNAME&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;See also: &lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/top-tags/&quot;&gt;Top tags&lt;/a&gt;&lt;/p&gt;
&lt;div data-callout-metadata=&quot;&quot; class=&quot;callout &quot; data-callout=&quot;tip&quot;&gt;&lt;div class=&quot;callout-title&quot;&gt;&lt;div class=&quot;callout-title-inner&quot;&gt; about this page&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;callout-content&quot;&gt;&lt;p&gt;This page is built on top of my &lt;a href=&quot;https://obsidian.md/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Obsidian&lt;/a&gt; notes, using the excellent &lt;a href=&quot;https://dg-docs.ole.dev/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Obsidian Digital Garden&lt;/a&gt; plugin and hosted on &lt;a href=&quot;https://vercel.com/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Vercel&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;&lt;/div&gt;
            </content>
            <link href="https://blog.eleni.co/" >
        </entry>
        <entry>
            <title>
                Top tags
                
            </title>
            <updated>2024-03-05T15:30:33.743Z</updated>
            <id>https://blog.eleni.co/garden/top-tags/</id>
            <content type="html">
                &lt;h1 id=&quot;top-tags&quot; tabindex=&quot;-1&quot;&gt;Top tags&lt;/h1&gt;
&lt;table class=&quot;block-language-dataview&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#how-to&quot;&gt;#how-to&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#python&quot;&gt;#python&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#aws&quot;&gt;#aws&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#embedded&quot;&gt;#embedded&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#microbit&quot;&gt;#microbit&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#django&quot;&gt;#django&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#outdated&quot;&gt;#outdated&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#cycling&quot;&gt;#cycling&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#dev&quot;&gt;#dev&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a class=&quot;tag&quot; onclick=&quot;toggleTagSearch(this)&quot; data-content=&quot;#elixir&quot;&gt;#elixir&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

            </content>
            <link href="https://blog.eleni.co/garden/top-tags/" >
        </entry>
        <entry>
            <title>
                The Time-Travelling Islands
                
            </title>
            <updated>2024-03-05T12:24:23.644Z</updated>
            <id>https://blog.eleni.co/garden/the-time-travelling-islands/</id>
            <content type="html">
                &lt;h1 id=&quot;the-time-travelling-islands&quot; tabindex=&quot;-1&quot;&gt;The Time-Travelling Islands&lt;/h1&gt;
&lt;p&gt;Original publication date: May 11, 2019&lt;/p&gt;
&lt;p&gt;Writing about your experience on time handling  in software as a developer seems to be the &amp;quot;write your own blog-engine&amp;quot; of blogging. So here goes 😀&lt;/p&gt;
&lt;h3 id=&quot;intro&quot; tabindex=&quot;-1&quot;&gt;Intro&lt;/h3&gt;
&lt;p&gt;One of features I&#39;ve worked on in a previous project, involved producing personalised  aggregations for things like sports performance and other personal data (ex. running, cycling, sleep patterns...). The particularity of this type of data was that it introduced a model of time ranges that don&#39;t only occur at a fixed point in time (at the User&#39;s time zone), but must always remain &amp;quot;relative&amp;quot; to our Users, wherever they might be.&lt;/p&gt;
&lt;p&gt;To better describe this with an example: Say one morning you wake up and you go for a run, while in London. If the next month you move to New York, then your run should still be categorised as a &amp;quot;morning&amp;quot; run, since you did it between 6am-11am local time, even if, by New York standards, it would have been an &amp;quot;late night&amp;quot; run.&lt;/p&gt;
&lt;p&gt;Similarly, if you cared about your sleep patterns, taking a log that says &amp;quot;you slept between 10pm and 6am&amp;quot; and converting it to another timezone, essentially makes it useless to you as a User.  If we moved it to another timezone, then it could suddenly appear as if you were sleeping in the middle of the day. So the range is only relevant when paired with the timezone in which it occurred, without converting it to the current timezone.&lt;/p&gt;
&lt;h3 id=&quot;time-for-motivation&quot; tabindex=&quot;-1&quot;&gt;Time for Motivation 🏃‍♀️&lt;/h3&gt;
&lt;p&gt;One of the applications for these aggregations was to run friendly competitions between users (this was a health-related platform, where maintaining motivation was a key target).&lt;/p&gt;
&lt;p&gt;The competitions would typically have a daily, weekly or monthly duration, and included users world-wide 🌍&lt;/p&gt;
&lt;p&gt;To take the example of a daily competition, we needed to observe the data of competitors on a single date, ex. 30/12/2011.&lt;/p&gt;
&lt;p&gt;One immediate challenge we faced was that &lt;strong&gt;a &amp;quot;day&amp;quot; on its own, without a specific time associated with it, cannot have a single timezone&lt;/strong&gt;. Sounds familiar? It&#39;s the same problem as describing what &amp;quot;morning&amp;quot; is! &amp;quot;Day&amp;quot;, &amp;quot;morning&amp;quot;, &amp;quot;month&amp;quot;... these time-ranges are always relative to our location.&lt;/p&gt;
&lt;p&gt;As we wanted to handle data for a specific &amp;quot;day&amp;quot;, before breaking the date down into time ranges for individual time zones, we first needed to answer another, more basic question:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;When does a day start and end, world-wide?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;So, what are the earliest and latest points that a date occurs, in UTC time?&lt;/p&gt;
&lt;h3 id=&quot;pacific-rim&quot; tabindex=&quot;-1&quot;&gt;Pacific Rim&lt;/h3&gt;
&lt;p&gt;One way to do that would be to find the earliest and latest timezone offsets, and use those as references. And we did exactly that. After a brief lookup, we found &lt;code&gt;Pacific/Kiritimati&lt;/code&gt; (Kiribati, Line Islands 🇰🇮) as the earliest offset, with &lt;code&gt;UTC+14&lt;/code&gt; year-round, and &lt;code&gt;Pacific/Apia&lt;/code&gt; (Apia, Samoa 🇼🇸), as the latest with a &lt;code&gt;UTC-11&lt;/code&gt; offset.&lt;/p&gt;
&lt;p&gt;And it worked! In fact things worked quite smoothly for us for a while...&lt;/p&gt;
&lt;h3 id=&quot;happy-new-year&quot; tabindex=&quot;-1&quot;&gt;Happy New Year!&lt;/h3&gt;
&lt;p&gt;...until we came back from Christmas holidays, on 2nd Jan 2012.&lt;/p&gt;
&lt;p&gt;Users were also back from holidays, and reporting issues on our support channels (what a great way to start you year 😭)!&lt;/p&gt;
&lt;p&gt;When we checked some of our logs and databases we realised that some of our aggregate data had not been generated for the last 3 days!&lt;/p&gt;
&lt;p&gt;After hours of tracking the problem, we realised that our date duration calculation was returning a wrong value. A day, it turns out, had now a duration of &lt;strong&gt;zero&lt;/strong&gt; hours. How was that even possible?&lt;/p&gt;
&lt;h3 id=&quot;back-to-the-future&quot; tabindex=&quot;-1&quot;&gt;Back to the Future&lt;/h3&gt;
&lt;p&gt;Most people don&#39;t usually read &lt;a href=&quot;https://web.archive.org/web/20220703115843/https://www.timeanddate.com/news/time/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Time Zone News&lt;/a&gt;. If we had, we would have noticed the following headline:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Samoa jumps forward in time&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Samoa will switch time zones by redrawing the international dateline. The change will occur at midnight on December 29, 2011, taking the Pacific island nation straight into December 31, 2011.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Turns out Samoa didn&#39;t want to be stuck in a timezone that&#39;s inconvenient for itself and its neighbours, so it decided to pull a Back To the Future manoeuvre, changing its timezone from &lt;code&gt;UTC-11&lt;/code&gt; to &lt;code&gt;UTC+14&lt;/code&gt;  and &lt;strong&gt;literally skipping an entire day 🍻&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;take-aways&quot; tabindex=&quot;-1&quot;&gt;Take aways&lt;/h3&gt;
&lt;p&gt;After quite a lot of digging and many &amp;quot;wtf&amp;quot; exclamations, we finally figured where the problem was coming from, quickly deployed a fix and no Time Lords needed to come and save us.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog-assets.eleni.co/media/saving_daylight.jpg&quot; alt=&quot;saving daylight postcard&quot; /&gt;&lt;/p&gt;
&lt;p&gt;What could we have done better?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We could have had an automated regular task checking for updates on our time zone library (in our case that would have been &lt;a href=&quot;https://web.archive.org/web/20220703115843/https://pypi.org/project/pytz/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;pytz&lt;/a&gt;). Time zone changes are rarely decided and implemented in short notice (though &lt;a href=&quot;https://web.archive.org/web/20220703115843/https://codeofmatt.com/on-the-timing-of-time-zone-changes/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;that&#39;s not always true&lt;/a&gt;). Staying up to date definitely makes you safer.&lt;/li&gt;
&lt;li&gt;Consider if your problem requires a time zone by its local name (ex. &lt;code&gt;Europe/London&lt;/code&gt;), or if an explicit UTC offset (ex. &lt;code&gt;UTC+1&lt;/code&gt;) is enough/appropriate. Most of the time you &lt;strong&gt;will&lt;/strong&gt; need to account for things like DST, so the offset will not be enough and can change, so the general advice is to use and store time zone names. But in &lt;strong&gt;some&lt;/strong&gt; cases, like ours, we could have hard-coded &lt;code&gt;UTC+14&lt;/code&gt; and &lt;code&gt;UTC-11&lt;/code&gt; without losing any information / risking breaking our product.&lt;/li&gt;
&lt;li&gt;Read the news (not just Hacker News)! Our work as product makers doesn&#39;t exist in a separate bubble from the rest of the world, as much as we like to limit the scope of what we do for simplicity&#39;s sake. Many large news sources &lt;a href=&quot;https://web.archive.org/web/20220703115843/https://www.bbc.co.uk/news/world-asia-16351377&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;like the BBC&lt;/a&gt; had also reported on the change.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;reading&quot; tabindex=&quot;-1&quot;&gt;Reading&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://web.archive.org/web/20220703115843/https://www.timeanddate.com/news/time/samoa-dateline.html&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Full article on Samoa&#39;s timezone change&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;If you haven&#39;t read this yet: &lt;a href=&quot;https://web.archive.org/web/20220703115843/https://zachholman.com/talk/utc-is-enough-for-everyone-right&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;UTC is enough for everyone, right?&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

            </content>
            <link href="https://blog.eleni.co/garden/the-time-travelling-islands/" >
        </entry>
        <entry>
            <title>
                Phoenix Plug.BasicAuth config at runtime
                
            </title>
            <updated>2025-06-01T16:02:09.459Z</updated>
            <id>https://blog.eleni.co/garden/phoenix-plug-basic-auth-config-at-runtime/</id>
            <content type="html">
                &lt;h1 id=&quot;phoenix-plug-basic-auth-config-at-runtime&quot; tabindex=&quot;-1&quot;&gt;Phoenix Plug.BasicAuth config at runtime&lt;/h1&gt;
&lt;p&gt;HTTP Basic Auth a simple way of restricting access to an app, without introducing a full-blown user authentication system.&lt;/p&gt;
&lt;p&gt;Luckily &lt;code&gt;Plug&lt;/code&gt; comes with &lt;code&gt;Plug.BasicAuth&lt;/code&gt; already built-in for us to use.&lt;/p&gt;
&lt;p&gt;This is the usage example from &lt;a href=&quot;https://hexdocs.pm/plug/Plug.BasicAuth.html&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;the docs&lt;/a&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;# lib/my_app.ex
import Plug.BasicAuth
plug :basic_auth, Application.compile_env(:my_app, :basic_auth)

# config/config.exs
config :my_app, :basic_auth, username: &amp;quot;hello&amp;quot;, password: &amp;quot;secret&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Seems simple enough!  In a real-life Phoenix app, we must probably:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;add the plug to our &lt;code&gt;Endpoint&lt;/code&gt; module, so we can protect all requests.&lt;/li&gt;
&lt;li&gt;read the config from env variables, so we don&#39;t include secrets in our codebase files.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;# lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
  # ...
  import Plug.BasicAuth, only: [basic_auth: 2]
  
  plug :basic_auth, Application.compile_env!(:my_app, :basic_auth)
  # ...
end

# config/config.exs
config :my_app, :basic_auth,
  username: System.get_env(&amp;quot;AUTH_USERNAME&amp;quot;, &amp;quot;user&amp;quot;)
  password: System.get_env(&amp;quot;AUTH_PASSWORD&amp;quot;, &amp;quot;password&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;the-problem&quot; tabindex=&quot;-1&quot;&gt;The problem&lt;/h2&gt;
&lt;p&gt;But what happens if we do it like this and load the config in &lt;code&gt;config.exs&lt;/code&gt;, as suggested?&lt;/p&gt;
&lt;p&gt;This might not seem to matter much while developing locally, but, as soon as we want to build and deploy our app, it means that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;To change the config we need to recompile and redeploy our code&lt;/li&gt;
&lt;li&gt;We need to have a way to make the secret variables available during build&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These  do sound like a bit of a pain...&lt;br /&gt;
👉 In fact, &lt;strong&gt;when it comes to env variables and external config, &lt;a href=&quot;https://hexdocs.pm/elixir/config-and-releases.html#configuration&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;the elixir docs actively discourage using &lt;code&gt;config.exs&lt;/code&gt;&lt;/a&gt;, in favour of &lt;code&gt;runtime.exs&lt;/code&gt;&lt;/strong&gt;, which allows us to read configuration during (surprise) runtime.&lt;/p&gt;
&lt;h2 id=&quot;let-s-fix-this&quot; tabindex=&quot;-1&quot;&gt;Let&#39;s fix this&lt;/h2&gt;
&lt;p&gt;So, can we just move our config into &lt;code&gt;runtime.exs&lt;/code&gt; and be done?&lt;/p&gt;
&lt;p&gt;Not right away! Our plug declaration is currently reading configuration at compile time. How do we know this? If you remember our &lt;code&gt;Endpoint&lt;/code&gt; definition from above:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;defmodule MyAppWeb.Endpoint do
  # ...
  plug :basic_auth, Application.compile_env!(:my_app, :basic_auth)
  # ...
end
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;We are using &lt;code&gt;Application.compile_env!/2&lt;/code&gt;. This reads the application environment at compile time&lt;/li&gt;
&lt;li&gt;And we are calling it in the module body. This is evaluated at compile time.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To fix this, we need to find alternatives for the two things above, so we can read the application environment at runtime:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use a runtime-alternative to &lt;code&gt;Application.compile_env!/2&lt;/code&gt;. E.g. &lt;code&gt;Application.fetch_env!/2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Call within a function body, where it will be evaluated at runtime.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So how do we do this? One way to do this would be to make our own wrapper Plug around the built-in one.&lt;/p&gt;
&lt;p&gt;We have two options here: &lt;a href=&quot;https://hexdocs.pm/phoenix/plug.html#function-plugs&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;as a function&lt;/a&gt;, or &lt;a href=&quot;https://hexdocs.pm/phoenix/plug.html#module-plugs&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;as a module&lt;/a&gt;.&lt;br /&gt;
Picking the simplest of the two, let&#39;s go with a function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;# lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
  # ...
  plug :basic_auth

  # ...
  def basic_auth(conn, _opts) do
    basic_auth_config = Application.fetch_env!(:my_app, :basic_auth)
    Plug.BasicAuth.basic_auth(conn, basic_auth_config)
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are now reading our config at runtime :)&lt;br /&gt;
Then we can just move our config to &lt;code&gt;runtime.exs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-elixir&quot;&gt;# config/runtime.exs
config :my_app, :basic_auth,
  username: System.get_env(&amp;quot;AUTH_USERNAME&amp;quot;, &amp;quot;user&amp;quot;)
  password: System.get_env(&amp;quot;AUTH_PASSWORD&amp;quot;, &amp;quot;password&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&#39;s it ✨&lt;/p&gt;
&lt;p&gt;PS: If we then want to deploy our app to something like &lt;a href=&quot;http://fly.io/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;fly.io&lt;/a&gt;, we can then configure our secrets like any other secret:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;fly secrets set AUTH_USERNAME=some-other-user AUTH_PASSWORD=very-secret-password
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or use whatever equivalent env variable/secret management system.&lt;/p&gt;
&lt;h2 id=&quot;key-takeaways&quot; tabindex=&quot;-1&quot;&gt;Key takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Be mindful of whether your code is reading configuration at compile time or at runtime.&lt;/li&gt;
&lt;li&gt;Most of the time you can and should choose to read at runtime.&lt;/li&gt;
&lt;li&gt;It&#39;s usually not too complicated to refactor your code to do so.&lt;/li&gt;
&lt;li&gt;See also: &lt;a href=&quot;https://hexdocs.pm/elixir/1.12/Application.html#module-compile-time-environment&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Application docs on compile-time environment&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

            </content>
            <link href="https://blog.eleni.co/garden/phoenix-plug-basic-auth-config-at-runtime/" >
        </entry>
        <entry>
            <title>
                Needle Alignment Tip - Singer Quantum Stylist 9960
                
            </title>
            <updated>2025-06-28T13:29:09.777Z</updated>
            <id>https://blog.eleni.co/garden/needle-alignment-tip-singer-quantum-stylist-9960/</id>
            <content type="html">
                &lt;h1 id=&quot;your-needle-alignment-is-not-broken-singer-quantum-stylist-9960&quot; tabindex=&quot;-1&quot;&gt;Your needle alignment is NOT broken - Singer Quantum Stylist 9960&lt;/h1&gt;
&lt;p&gt;While working on a project, I was doing something dodgy like trying to sew one million layers of fabric using an ancient needle. Not sure when the last time I cleaned my machine was. This resulted in the machine skipping and all the thread tangling up.&lt;/p&gt;
&lt;p&gt;No worries, I untangled it all, cleaned up a bit, changed the needle, and so I was back on track. Only I wasn&#39;t: &lt;strong&gt;when I turned the machine back on, the needle would not align&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Things I tried that didn&#39;t work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Turning it off and on again.&lt;/li&gt;
&lt;li&gt;Changing the stitch setting&lt;/li&gt;
&lt;li&gt;Worrying I broke the machine&lt;/li&gt;
&lt;li&gt;Reading every manual on the internet&lt;/li&gt;
&lt;li&gt;Watching every YouTube repair video where they take half the sewing machine apart and put it back together&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first time this happened, I decided to call it a day. Next day I set up the machine again, and was surprised to see everything worked.&lt;/p&gt;
&lt;p&gt;Theory: it was definitely overheating.&lt;br /&gt;
Reality: it was overheating of my brain.&lt;/p&gt;
&lt;p&gt;The second time this happened, I figured out what the actual problem was:&lt;br /&gt;
&lt;strong&gt;The needle&#39;s position was too low&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;How? As I was manually moving the needle to slowly untangle things, I did not put the needle back to the top-most position.&lt;/p&gt;
&lt;p&gt;🪡 &lt;strong&gt;If the needle is too low, the machine will not change the alignment&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This is presumably a protection mechanism, so that you don&#39;t change alignment while the needle is close to the foot, or through fabric.&lt;/p&gt;
&lt;p&gt;⬆️ &lt;strong&gt;Takeaway 1: always set the needle to the top-most position&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Isn&#39;t this sewing machine 101? Maybe. Sometimes when using a digital sewing machine, it offers automations that you take for granted (e.g. the Singer Quantum Stylist 9960 has a ✂️ button that will reset the needle position and cut the thread for you).&lt;/p&gt;
&lt;p&gt;🧠 &lt;strong&gt;Takeaway 2: just because you have a fancy machine, doesn&#39;t mean you can forget the basics&lt;/strong&gt;.&lt;/p&gt;

            </content>
            <link href="https://blog.eleni.co/garden/needle-alignment-tip-singer-quantum-stylist-9960/" >
        </entry>
        <entry>
            <title>
                Microbit Project 3 - Plants have feelings too
                
            </title>
            <updated>2024-03-05T12:52:27.121Z</updated>
            <id>https://blog.eleni.co/garden/microbit-project-3-plants-have-feelings-too/</id>
            <content type="html">
                &lt;h1 id=&quot;project-3-plants-have-feelings-too&quot; tabindex=&quot;-1&quot;&gt;Project # 3: Plants have feelings too&lt;/h1&gt;
&lt;p&gt;Original publication date: Dec. 30, 2015&lt;/p&gt;
&lt;p&gt;I have recently been pretty bad at keeping my house plants alive. I need to add reminders to take care of my plants, and I usually end up ignoring them as &amp;quot;I&#39;ll do it when I get home&amp;quot;, and then I almost never do. &lt;strong&gt;Wouldn&#39;t it be nice if the plant could tell you that it needs attention?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Plants don&#39;t have faces to express themselves, so what if the micro:bit could work for that purpose?&lt;/p&gt;
&lt;p&gt;Excluding cacti, the one thing that plants usually need the most is ground moisture. &lt;em&gt;How is moisture measured?&lt;/em&gt; &lt;strong&gt;The internet says that conductivity is a pretty good indicator.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So with a simple Ohmmeter we could measure the increase/decrease in resistance and by setting a few reasonable thresholds we can tell what the resistance corresponds to in terms of moisture levels.&lt;/p&gt;
&lt;p&gt;Tools used:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3 cables with alligator clips&lt;/li&gt;
&lt;li&gt;2 galvanised (I think) nails.&lt;/li&gt;
&lt;li&gt;1 resistor (~1.2KΩ)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The nails were used as ground probes, one connected to power and one connected to &lt;code&gt;pin0&lt;/code&gt;. The resistor was used as the &amp;quot;known&amp;quot; resistor for the ohmmeter. To replicate this you could use any resistor/ground probes/cabling. Just make sure the code is adapted for the resistor and that the probes are resistant to corrosion.&lt;/p&gt;
&lt;p&gt;Also it goes without saying that you should keep the micro:bit at a safe distance from water and dirt ;)&lt;/p&gt;
&lt;p&gt;In terms of programming, I set the micro:bit to display a happy, ok or sad face depending on the value of the resistor. Finding the correct thresholds was tricky as they can vary depending on the soil, distance between the probes etc. (&lt;a href=&quot;https://github.com/Geekfish/microbit-week/blob/master/happy_plant.py&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;code&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;So, without further ado here&#39;s a watering demo of one of my plants (which I kept without water for a few days for that purpose as you can see from its horrible condition!):&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://i.giphy.com/26tPmvQGLAWMIOMr6.gif&quot; alt=&quot;plants have feelings&quot; /&gt;&lt;/p&gt;
&lt;p&gt;As you can see with a bit of watering the plant becomes &amp;quot;OK&amp;quot; and with a bit more it becomes &amp;quot;Happy&amp;quot; ;)&lt;/p&gt;
&lt;p&gt;A few potential extensions to this project (probably more important for outdoor plants...):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Light detector&lt;/li&gt;
&lt;li&gt;Temperature detector&lt;/li&gt;
&lt;/ul&gt;

            </content>
            <link href="https://blog.eleni.co/garden/microbit-project-3-plants-have-feelings-too/" >
        </entry>
        <entry>
            <title>
                Microbit Project 2 - Snowflakes
                
            </title>
            <updated>2024-03-03T14:50:43.006Z</updated>
            <id>https://blog.eleni.co/garden/microbit-project-2-snowflakes/</id>
            <content type="html">
                &lt;h1 id=&quot;project-2-snowflakes-almost&quot; tabindex=&quot;-1&quot;&gt;Project # 2: Snowflakes (almost)&lt;/h1&gt;
&lt;p&gt;Original publication date: Dec. 30, 2015&lt;/p&gt;
&lt;p&gt;Since Christmas often means snow (at least for us living in the northern hemisphere) I thought to try to generate some snowflakes on the microbit.&lt;/p&gt;
&lt;p&gt;Since snowflakes are symmetrical, we should only have to create a 3x3 corner and rotate that 45° at a time that to get the full shape. End result might resemble a star-wars spaceship instead of snowflakes. Equally seasonal?&lt;/p&gt;
&lt;p&gt;Here&#39;s how it looked like:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://i.giphy.com/d2ZjJTBfZ2WMQuyI.gif&quot; alt=&quot;snowflakes demo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;and &lt;a href=&quot;https://github.com/Geekfish/microbit-week/blob/master/snowflakes.py&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;here&#39;s the code&lt;/a&gt;.&lt;/p&gt;

            </content>
            <link href="https://blog.eleni.co/garden/microbit-project-2-snowflakes/" >
        </entry>
        <entry>
            <title>
                Microbit Project 1 - Dice roller
                
            </title>
            <updated>2024-03-03T14:50:19.062Z</updated>
            <id>https://blog.eleni.co/garden/microbit-project-1-dice-roller/</id>
            <content type="html">
                &lt;h1 id=&quot;project-1-dice-roller&quot; tabindex=&quot;-1&quot;&gt;Project # 1: Dice roller&lt;/h1&gt;
&lt;p&gt;Original publication date: Dec. 22, 2015&lt;/p&gt;
&lt;p&gt;So the plan is to make a simple dice roller application.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The buttons should cycle through the various dice from a simple standard RPG poly set (d4, d6, d8, d10, d12, d20)&lt;/li&gt;
&lt;li&gt;Once a die is selected the player should be able to roll it by shaking the micro:bit.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Possible difficulties:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Find a way to distinguish the rolling motion from other types of movement. My gut tells me this will be through detecting certain types of acceleration.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;continued&quot; tabindex=&quot;-1&quot;&gt;Continued...&lt;/h2&gt;
&lt;p&gt;I didn&#39;t get the chance to work on this yesterday due to Christmas preparations but today I got a couple of hours playing with the Micro:bit again :)&lt;/p&gt;
&lt;p&gt;Back to detecting dice rolling, turns out this wasn&#39;t too hard but I did need to play a little bit with calibrating the thresholds for detecting consecutive changes in acceleration. There are probably more precise ways to do this, but it works for now!&lt;/p&gt;
&lt;p&gt;I created a &lt;a href=&quot;https://github.com/Geekfish/microbit-week/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;repo&lt;/a&gt; for the rest of the Micro:bit projects and &lt;a href=&quot;https://github.com/Geekfish/microbit-week/blob/master/dicer_roller.py&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;this is the code for the dice roller&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://i.giphy.com/d2Z6C1b3VckTDHpe.gif&quot; alt=&quot;dice roller demo&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I just realised this is very similar to what Martin O&#39;Hanlon did with his &lt;a href=&quot;http://www.stuffaboutcode.com/2015/11/microbit-magic-8-ball-with-micropython.html&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;8-ball shuffle&lt;/a&gt;, our solution was actually pretty similar. I had to add some time thresholds as well, otherwise the shake event ended up happening way too easily and offset.&lt;/p&gt;

            </content>
            <link href="https://blog.eleni.co/garden/microbit-project-1-dice-roller/" >
        </entry>
        <entry>
            <title>
                Meet Mei-Xiu
                
            </title>
            <updated>2024-03-03T10:41:01.061Z</updated>
            <id>https://blog.eleni.co/garden/meet-mei-xiu/</id>
            <content type="html">
                &lt;h1 id=&quot;meet-mei-xiu&quot; tabindex=&quot;-1&quot;&gt;Meet Mei-Xiu&lt;/h1&gt;
&lt;p&gt;Original publication date: Dec. 22, 2015&lt;/p&gt;
&lt;p&gt;Sent to me by its previous host, Stewart Watkiss. Haven&#39;t flashed the device yet so it looks like the robot controlling hex is still loaded.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.eleni.co/img/user/Files/2dbf0e73-f016-4cbe-9ecf-5aa7107cdf00.JPG.640x640_q85.jpg&quot; alt=&quot;2dbf0e73-f016-4cbe-9ecf-5aa7107cdf00.JPG.640x640_q85.jpg&quot; /&gt;&lt;/p&gt;
&lt;h1 id=&quot;environment-setup&quot; tabindex=&quot;-1&quot;&gt;Environment setup&lt;/h1&gt;
&lt;p&gt;Started using &lt;a href=&quot;https://github.com/ntoll/upyed&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;code&gt;upyed&lt;/code&gt;&lt;/a&gt; and Tom Viner&#39;s very convenient &lt;a href=&quot;https://github.com/tomviner/upyflashed&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;code&gt;upyflashed&lt;/code&gt;&lt;/a&gt;. The tool was exiting with a trace in the event of no hex files being available, so I &lt;a href=&quot;https://github.com/tomviner/upyflashed/pull/1&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;opened a PR&lt;/a&gt; to make this a bit better.&lt;/p&gt;
&lt;p&gt;EDIT: Turns out my &amp;quot;fix&amp;quot; was skipping the &lt;code&gt;sleep&lt;/code&gt; step which caused the loop cycles to be a bit too fast. Good thing Tom caught that 😉&lt;/p&gt;
&lt;h1 id=&quot;it-s-xmas&quot; tabindex=&quot;-1&quot;&gt;It&#39;s Xmas&lt;/h1&gt;
&lt;p&gt;The Micro:bit gets a warm Christmas welcome by some of the other flashing LEDs in our house.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;http://i.giphy.com/26tP0oYwIJ184Kf4Y.gif&quot; alt=&quot;ho ho ho&quot; /&gt;&lt;/p&gt;

            </content>
            <link href="https://blog.eleni.co/garden/meet-mei-xiu/" >
        </entry>
        <entry>
            <title>
                Karvonen HR training zones
                
            </title>
            <updated>2024-03-10T00:00:02.789Z</updated>
            <id>https://blog.eleni.co/garden/karvonen-hr-training-zones/</id>
            <content type="html">
                &lt;h1 id=&quot;karvonen-hr-training-zones&quot; tabindex=&quot;-1&quot;&gt;Karvonen HR training zones&lt;/h1&gt;
&lt;h2 id=&quot;background&quot; tabindex=&quot;-1&quot;&gt;Background&lt;/h2&gt;
&lt;p&gt;When training in activities like running or cycling, the concept of &lt;em&gt;training &amp;quot;zones&amp;quot;&lt;/em&gt; is often used, in order to prescribe different levels of &lt;em&gt;intensity&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;There is value in training at different levels of intensity (low, moderate, aerobic, unaerobic etc), depending on what an athlete wants to achieve in a specific training session or longer term.&lt;/p&gt;
&lt;h2 id=&quot;the-general-method&quot; tabindex=&quot;-1&quot;&gt;The general method&lt;/h2&gt;
&lt;p&gt;There are different ways of figuring out these zones. The most popular one, often also used by fitness programs and apps, relies on knowing one&#39;s maximum heart rate and with each zone being a percentage of it.&lt;/p&gt;
&lt;p&gt;E.g. for someone with a max heart rate of 185bpm, training at 70% intensity would simply be:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;185 * 0.7 = 129.5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;this-doesn-t-always-work&quot; tabindex=&quot;-1&quot;&gt;This doesn&#39;t always work&lt;/h3&gt;
&lt;p&gt;Even when we have the max heart rate figured out, for some people these zones don&#39;t seem to correspond to how they actually experience different levels of intensity.&lt;/p&gt;
&lt;p&gt;This may be due to them being highly trained athletes, well adapted to higher levels of intensity, or just regular physiological differences among individuals.&lt;/p&gt;
&lt;p&gt;In my case, even at my fittest, I could never achieve the expected low intensity zones calculated with this method – my heart rate would always shoot past them  at the slightest effort, even though I could sustain that effort almost indefinitely. What gives?&lt;/p&gt;
&lt;h2 id=&quot;alternative-the-karvonen-method&quot; tabindex=&quot;-1&quot;&gt;Alternative: the Karvonen method&lt;/h2&gt;
&lt;p&gt;The Karvonen method is an alternative, only slightly more elaborate method of calculating training zones.&lt;/p&gt;
&lt;p&gt;This method calculates percentages based on &lt;strong&gt;heart rate reserve&lt;/strong&gt;. In other words, takes both the &lt;strong&gt;max heart rate&lt;/strong&gt; and the &lt;strong&gt;resting heart rate&lt;/strong&gt; into account.&lt;/p&gt;
&lt;p&gt;The full formula is defined as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;((Maximum HR – Resting HR) * Intensity) + Resting HR
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So for someone with 185bpm max and 62bpm resting heart rate, to train at 70% intensity, we would have:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;((185 - 62) * 0.7) + 62 = 148
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This yields a very different result from the general method above (&lt;em&gt;almost 20bmp more for the same intensity&lt;/em&gt;)!&lt;/p&gt;
&lt;p&gt;While this is again a formula that can be imprecise, it seems to have worked well for me, providing much more realistic ranges that I can follow ✨  If you haven&#39;t stumbled upon it and you are frustrated with the default zones provided by your fitness app, it could be worth a try!&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.omnicalculator.com/health/karvonen-formula&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Calculator with sources&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note] How do I find my max HR?&lt;br /&gt;
It&#39;s not always easy to calculate one&#39;s true maximum heart rate. For many people, a rough calculation based on their age &lt;code&gt;(220 - age)&lt;/code&gt; may provide a good ballpark estimate.&lt;/p&gt;
&lt;/blockquote&gt;

            </content>
            <link href="https://blog.eleni.co/garden/karvonen-hr-training-zones/" >
        </entry>
        <entry>
            <title>
                Hosting Obsidian S3 media publicly
                
            </title>
            <updated>2024-08-09T12:06:35.032Z</updated>
            <id>https://blog.eleni.co/garden/hosting-obsidian-s3-media-publicly/</id>
            <content type="html">
                &lt;h1 id=&quot;hosting-obsidian-on-media-publicly&quot; tabindex=&quot;-1&quot;&gt;Hosting Obsidian on media publicly&lt;/h1&gt;
&lt;p&gt;There are multiple options when we want to host media and larger files and serve them on the web.&lt;/p&gt;
&lt;p&gt;Here&#39;s my setup for making any asset available through S3 (storage) and &lt;strong&gt;Cloudfront&lt;/strong&gt; (CDN) on AWS, to take advantage of my existing account and the numerous tools and guides that already exist.&lt;/p&gt;
&lt;h2 id=&quot;public-bucket-no-thanks&quot; tabindex=&quot;-1&quot;&gt;Public bucket? No thanks!&lt;/h2&gt;
&lt;p&gt;Many similar tutorials suggest allowing public read access to S3 buckets. &lt;a href=&quot;https://medium.com/@maciej.pocwierz/how-an-empty-s3-bucket-can-make-your-aws-bill-explode-934a383cb8b1&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;This may not be a great idea&lt;/a&gt;. In this walkthrough we&#39;re going to use Cloudfront together with S3, to manage the content delivery. This means that the S3 bucket itself doesn&#39;t need to be public. Content is published only through Cloudfront.&lt;/p&gt;
&lt;h2 id=&quot;rough-steps&quot; tabindex=&quot;-1&quot;&gt;Rough steps&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/obsidian-public-media/setup-s3-bucket-for-use-with-cloudfront/&quot;&gt;Setup S3 bucket for use with Cloudfront&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/obsidian-public-media/setup-custom-aws-cloud-front-cname/&quot;&gt;Setup custom AWS CloudFront CNAME&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/obsidian-public-media/setup-an-iam-user-to-manage-an-s3-bucket/&quot;&gt;Setup an IAM user to manage an S3 bucket&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/obsidian-public-media/upload-the-media-to-your-s3-bucket/&quot;&gt;Upload the media to your S3 bucket&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;notes&quot; tabindex=&quot;-1&quot;&gt;Notes&lt;/h2&gt;

            </content>
            <link href="https://blog.eleni.co/garden/hosting-obsidian-s3-media-publicly/" >
        </entry>
        <entry>
            <title>
                GDPR on top of Django&#39;s model Collector
                
            </title>
            <updated>2024-03-05T12:16:52.390Z</updated>
            <id>https://blog.eleni.co/garden/gdpr-on-top-of-django-s-model-collector/</id>
            <content type="html">
                &lt;h1 id=&quot;gdpr-on-top-of-django-s-model-collector&quot; tabindex=&quot;-1&quot;&gt;GDPR on top of Django&#39;s model Collector&lt;/h1&gt;
&lt;p&gt;Original publication date: Sep. 30 2018&lt;br /&gt;
&lt;a href=&quot;https://speakerdeck.com/geekfish/gdpr-plus-django-model-collector&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Django meetup presentation&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In the rise of GDPR many organisations have started implementing more and more features to deal with &amp;quot;housekeeping&amp;quot; of user data, compliance with new data processing/retention policies and explicit requests for data export or removal.&lt;/p&gt;
&lt;p&gt;In practice, it can be hard to figure out all the possible connections of related data when given a specific user entity. In the context of complex relational databases, we are probably faced with inspecting large trees of relations and constraints.&lt;/p&gt;
&lt;p&gt;Django tries to partially solve this problem by defining and managing most relations and constraints on the application level. Users of the Django admin may be familiar with this delete confirmation view:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog-assets.eleni.co/media/django_admin_delete.png&quot; alt=&quot;Django Admin user deletion view&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is possible because Django contains an object collector that will lookup those relations. When we run &lt;code&gt;user.delete()&lt;/code&gt; Django will not just execute&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;DELETE FROM `myapp_users` WHERE `myapp_users`.`id` = x
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;but will first traverse all relations and collect all directly and indirectly related objects, which it will then clean up as appropriate. According to &lt;a href=&quot;https://docs.djangoproject.com/en/2.1/ref/models/fields/#django.db.models.CASCADE&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Django docs&lt;/a&gt; no ON DELETE CASCADE rule is ever defined in the DB, it is just emulated by the ORM.&lt;/p&gt;
&lt;p&gt;While it can sometimes be frustrating to not have the DB fully manage those relations, we can, in some cases, use this Django feature to our advantage.&lt;/p&gt;
&lt;h3 id=&quot;collect-data-for-export&quot; tabindex=&quot;-1&quot;&gt;Collect data for export&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Can we find out what data we have on this user?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This question pops up more and more frequently since the implementation of GDPR. The common use-case is when a user asks for an export of their data.&lt;/p&gt;
&lt;p&gt;If we wanted to use the Django collector to get all related models for a given model, we could write something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.contrib.admin.utils import NestedObjects
  
def collect_user_data(user):
    collector = NestedObjects(using=&amp;quot;replica&amp;quot;)
    collector.collect([user])
    return collector.data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This would return all data related to our user instance (as long as the ORM knows about the relations).&lt;/p&gt;
&lt;p&gt;This can be built upon for more complex cases:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.contrib.admin.utils import NestedObjects

# Imaginary module, used as an example
from myapp.housekeeping import collect, IP_ASSOCIATED_MODELS 


class CustomUserCollector(NestedObjects):
    def __init__(self, user, *args, **kwargs):
        self.user = user
        if &#39;using&#39; not in kwargs:
            kwargs[&#39;using&#39;] = &amp;quot;replica&amp;quot;
        super(CustomUserCollector, self).__init__(*args, **kwargs)

    def collect_from_db(self):
        self.collect([self.user])
        
        # Maybe we have some models that don&#39;t have a FK to user,
        # but are still somehow associated...
        for model in IP_ASSOCIATED_MODELS:
            self.collect(
                model.objects.filter(ip__in=self.user.known_ips))
    
    def collect_from_nonrel(self):
        # Maybe we have other types of stores in our system
        # that may contain more user data...
        self.data[&#39;couchdb&#39;] = collect.from_couchdb(user)
        self.data[&#39;redis_temp&#39;] = collect.from_redis(user)

    def collect_remote(self):
        # Maybe there is an internal API where we store user data
        # with its own export mechanism
        self.data[&#39;some_ext_service&#39;] = collect.from_api(user)

    def collect_all():
        self.collect_from_db()
        self.collect_from_nonrel()
        self.collect_remote()


def collect_user_data(user):
    collector = CustomUserCollector(user)
    collector.collect_all()
    return collector.data
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;report-on-deleted-data&quot; tabindex=&quot;-1&quot;&gt;Report on deleted data&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Can we keep track of how much data we&#39;re deleting?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So, we&#39;ve been happily collecting and exporting data, but now it&#39;s time to delete some of it. Maybe we need to remove accounts after receiving deletion requests from users, or we may need to tidy up after data that&#39;s no longer used.&lt;/p&gt;
&lt;p&gt;Deleted data is data we no longer need to really worry about (DISCLAIMER: I enjoy deleting stuff, including code).&lt;/p&gt;
&lt;p&gt;If you simply remove an instance through the ORM, you will get back a summary of what was deleted. &lt;a href=&quot;https://docs.djangoproject.com/en/2.1/topics/db/queries/#deleting-objects&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;From the Django docs&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;Entry.objects.filter(pub_date__year=2005).delete()
# (5, {&#39;webapp.Entry&#39;: 5})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Behind the scenes, django uses the same model collector to figure out what needs to be deleted and create a report.&lt;/p&gt;
&lt;p&gt;A more complex deletion script could look something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def delete_all_the_things():
    inactive_users = fetch_inactive_users()
    inactive_users.delete()
    
    users_pending_delete = fetch_pending_deletes()  
    users_pending_delete.delete()
      
    for mailing_list in mailing_lists:
        unconfirmed_subs = fetch_unconfirmed_subs(mailing_list)
        unconfirmed_subs.delete()  

    # ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;etc.&lt;/p&gt;
&lt;p&gt;If we wanted to check how many things we&#39;ve just deleted, we would need to collect the output of each &lt;code&gt;.delete()&lt;/code&gt; or log the individual statements.&lt;br /&gt;
We could also collect them and produce a summary, using an accumulator:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class DeleteStats(object):
    def __init__(self, delete_output=None):
        super(DeleteStats, self).__init__()
        self.total_removed = 0
        self.removed_by_model = defaultdict(int)

        if delete_output:
            self.extend(delete_output)

    def extend(self, delete_output):
        total_removed, removed_by_model = delete_output

        self.total_removed += total_removed
        for _model, _count in removed_by_model.iteritems():
            self.removed_by_model[_model] += _count

        return self

    def __getitem__(self, index):
        # Pretend to be a tuple to match how the django API returns deletes
        if index == 0:
            return self.total_removed
        elif index == 1:
            return self.removed_by_model
        raise IndexError()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://gist.github.com/Geekfish/5b171fd76f0990343d06ac3194497383&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;also available as a Gist with mini-docs&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import logging  
from myapp.housekeeping import DeleteStats

logger = logging.getLogger(__name__)

def delete_all_the_things(): 
    stats = DeleteStats()  
 
    inactive_users = fetch_inactive_users()
    stats.extend(inactive_users.delete())
    
    users_pending_delete = fetch_pending_deletes()  
    stats.extend(users_pending_delete.delete())
          
    for mailing_list in mailing_lists:
        unconfirmed_subs = fetch_unconfirmed_subs(mailing_list)
        stats.extend(unconfirmed_subs.delete())  
  
    # ...  
    total_deleted, removed_by_model = stats  

    logger.info(&amp;quot;Deleted {} objects in total&amp;quot;.format(total_removed))

    for model_name, count in removed_by_model.iteritems():
          logger.info(&amp;quot;- {model_name} deleted: {count}&amp;quot;.format(
                model_name=model_name, count=count))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IRL, instead of one long function, you could pass the accumulator into your cleanup functions, implement your own re-usable logging etc.&lt;/p&gt;
&lt;h3 id=&quot;gotchas&quot; tabindex=&quot;-1&quot;&gt;Gotchas&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;. When the collector traverses and fetches all these relations, things can get pretty slow. On bulk-deletes, Django will detect if a model requires further cleanup of related objects and can end up &lt;strong&gt;iterating&lt;/strong&gt; over each object marked for deletion (have a look at &lt;code&gt;django.db.models.deletion.Collector#delete&lt;/code&gt; if you are curious to see how it works).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Over-relying on automated data discovery&lt;/strong&gt;:&lt;br /&gt;
One size might not fit all (including all of your Django models in all their different shapes...).&lt;br /&gt;
&lt;strong&gt;Auto-discovered data may not be relevant&lt;/strong&gt;. This sounds a bit silly, but imagine an export of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &amp;quot;id&amp;quot;: 12345,
    &amp;quot;first_name&amp;quot;: &amp;quot;Bob&amp;quot;,
    &amp;quot;password&amp;quot;: &amp;quot;pbkdf2_sha256$...rlP2U5X9BuFEGF6603k=&amp;quot;,  
    ... 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The user does not need to know their password hash. They may not need to know meta data related to some UI setting, or denormalised data relevant only for performance enhancement. In addition:&lt;br /&gt;
&lt;strong&gt;Auto-discovered data may require more context&lt;/strong&gt;. Imagine an export of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &amp;quot;id&amp;quot;: 12345,
    &amp;quot;first_name&amp;quot;: &amp;quot;Linda&amp;quot;,
    &amp;quot;recent_actions&amp;quot;: [
        &amp;quot;id&amp;quot;: 5,
        &amp;quot;timestamp&amp;quot;: 1538317086,
    ],  
    ... 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without any additional context, action &lt;code&gt;&amp;quot;id&amp;quot;: 5&lt;/code&gt; means absolutely nothing to the user or any 3rd party.&lt;/p&gt;
&lt;p&gt;For both of the above, consider creating an explicit &lt;strong&gt;transformation/serialisation layer&lt;/strong&gt; that can add or remove fields as appropriate (I&#39;ve used &lt;a href=&quot;http://www.django-rest-framework.org/api-guide/serializers/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;&lt;code&gt;Serializer&lt;/code&gt; from the DRF&lt;/a&gt; for that purpose in the past, probably worthy of its own post)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, &lt;strong&gt;a one-off automated solution is not a replacement for solid process&lt;/strong&gt;. New features will be added. New services will be deployed. Data will change form and move about. It is worth having a data-housekeeping checklist to reference in every change, to ensure that your automated processes stay up to date.&lt;/p&gt;

            </content>
            <link href="https://blog.eleni.co/garden/gdpr-on-top-of-django-s-model-collector/" >
        </entry>
        <entry>
            <title>
                Django ORM - Order of query evaluation
                
            </title>
            <updated>2024-03-03T14:50:10.628Z</updated>
            <id>https://blog.eleni.co/garden/django-orm-order-of-query-evaluation/</id>
            <content type="html">
                &lt;h1 id=&quot;django-orm-order-of-query-evaluation&quot; tabindex=&quot;-1&quot;&gt;Django ORM - Order of query evaluation&lt;/h1&gt;
&lt;p&gt;Original publication date: Aug. 12, 2016&lt;/p&gt;
&lt;p&gt;When working with an ORM it is often easy to forget that whatever you write is ultimately translated to SQL queries.&lt;/p&gt;
&lt;p&gt;Consider this script:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;bookings = task.contractor.bookings.all()
task.delete()
bookings.delete()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will run with no errors.&lt;/p&gt;
&lt;p&gt;As you would expect&lt;code&gt;task&lt;/code&gt; &lt;strong&gt;will be deleted&lt;/strong&gt;, however the &lt;code&gt;bookings&lt;/code&gt; &lt;strong&gt;will persist in the db&lt;/strong&gt;! What&#39;s going on?&lt;/p&gt;
&lt;p&gt;The code above will result in 2 SQL queries:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;One that deletes the task based on the task id&lt;/li&gt;
&lt;li&gt;One that deletes bookings that appear as a foreign key to the related contractor table, via the related tasks table filtered by... the same task id that was just deleted!&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As the task is deleted in the 1st query, then the 2nd query will yield no results (as the specific task id will no longer exist).&lt;/p&gt;
&lt;p&gt;For this to work we just need to reverse the order of operations:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;bookings = task.contractor.bookings.all()
bookings.delete()
task.delete()
&lt;/code&gt;&lt;/pre&gt;

            </content>
            <link href="https://blog.eleni.co/garden/django-orm-order-of-query-evaluation/" >
        </entry>
        <entry>
            <title>
                A week with micro-bit
                
            </title>
            <updated>2024-03-03T10:41:50.188Z</updated>
            <id>https://blog.eleni.co/garden/a-week-with-micro-bit/</id>
            <content type="html">
                &lt;h1 id=&quot;a-week-with-the-bbc-micro-bit&quot; tabindex=&quot;-1&quot;&gt;A week with the BBC micro:bit&lt;/h1&gt;
&lt;p&gt;Mei-Xiu (RIP) was a &lt;a href=&quot;https://www.bbc.co.uk/mediacentre/mediapacks/microbit&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;BBC micro:bit &lt;/a&gt;device running MicroPython, one of five devices that were sent out to travel the world and the python community.&lt;/p&gt;
&lt;p&gt;I had the chance to play with it during the xmas period in 2015.&lt;/p&gt;
&lt;p&gt;This is a series of small posts I made at the time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/meet-mei-xiu/&quot;&gt;Meet Mei-Xiu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/microbit-project-1-dice-roller/&quot;&gt;Microbit Project 1 - Dice roller&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/microbit-project-2-snowflakes/&quot;&gt;Microbit Project 2 - Snowflakes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;internal-link&quot; target=&quot;&quot; data-note-icon=&quot;&quot; href=&quot;https://blog.eleni.co/garden/microbit-project-3-plants-have-feelings-too/&quot;&gt;Microbit Project 3 - Plants have feelings too&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;http://microworldtour.github.io/about.html&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Read the whole story behind the project&lt;/a&gt; or follow the entire trip at &lt;a href=&quot;https://microworldtour.github.io/microbit/meixiu.html&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Micro World Tour&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Originally also posted on &lt;a href=&quot;https://thoughtstreams.io/Geekfish/a-week-with-microbit/&quot; target=&quot;_blank&quot; class=&quot;external-link&quot;&gt;Thoughtstreams&lt;/a&gt;.&lt;/p&gt;

            </content>
            <link href="https://blog.eleni.co/garden/a-week-with-micro-bit/" >
        </entry>
</feed>
