<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Robert Tidball</title>
    <description>The latest articles on Forem by Robert Tidball (@roberttidball).</description>
    <link>https://forem.com/roberttidball</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3608997%2F531d13ca-ef6b-4bba-8ee2-56cac876c320.jpeg</url>
      <title>Forem: Robert Tidball</title>
      <link>https://forem.com/roberttidball</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/roberttidball"/>
    <language>en</language>
    <item>
      <title>Introducing the FXMacroData Economic Data Embed Widget</title>
      <dc:creator>Robert Tidball</dc:creator>
      <pubDate>Wed, 03 Dec 2025 13:34:36 +0000</pubDate>
      <link>https://forem.com/fxmacrodata/introducing-the-fxmacrodata-economic-data-embed-widget-3oin</link>
      <guid>https://forem.com/fxmacrodata/introducing-the-fxmacrodata-economic-data-embed-widget-3oin</guid>
      <description>&lt;p&gt;Today, we’re launching a major new feature on &lt;a href="https://fxmacrodata.com/" rel="noopener noreferrer"&gt;FXMacroData&lt;/a&gt; — fully embeddable, real-time charts that you can drop into any website, blog, or research portal with a single copy-and-paste.&lt;/p&gt;

&lt;p&gt;Each chart on FXMacroData now includes a dedicated “Embed Graph” button. Clicking it reveals a ready-to-paste HTML snippet.&lt;/p&gt;

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

&lt;p&gt;You can view the full data and chart on the dashboard here: &lt;a href="https://fxmacrodata.com/dashboard/EUR_USD" rel="noopener noreferrer"&gt;https://fxmacrodata.com/dashboard/EUR_USD&lt;/a&gt; To embed the chart above on your own site, use this snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!-- FXMacroData Chart Embed --&amp;gt;
&amp;lt;div
    id="fxmacro-carryChart"
    style="width: 100%; max-width: 800px; margin: 20px auto;"
&amp;gt;
    &amp;lt;iframe
        src="https://fxmacrodata.com/dashboard/embed/EUR_USD/carryChart"
        width="100%"
        height="450"
        frameborder="0"
        style="border: 1px solid #e5e7eb; border-radius: 8px;"
    &amp;gt;&amp;lt;/iframe&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How Embeds Work
&lt;/h2&gt;

&lt;p&gt;Embedding is as simple as copying the snippet and pasting it into your page. Each FXMacroData embed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updates in real time as new announcements are released&lt;/li&gt;
&lt;li&gt;Requires zero maintenance or API integration&lt;/li&gt;
&lt;li&gt;Loads quickly thanks to a lightweight hosting layer&lt;/li&gt;
&lt;li&gt;Automatically adapts to your layout and device size&lt;/li&gt;
&lt;li&gt;Built for Analysts, Traders, and Educators&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether you’re building models, writing research, teaching, or running a fintech product, these embeds let you present live macroeconomic data without engineering work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designed for Reliability and Performance
&lt;/h2&gt;

&lt;p&gt;Each widget runs on the same architecture powering the FXMacroData dashboards, backed by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time streaming data from our announcement pipeline&lt;/li&gt;
&lt;li&gt;Optimized caching for low latency&lt;/li&gt;
&lt;li&gt;Cloud Run auto-scaling for high-volatility events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your embeds stay fast, accurate, and stable — even during major releases when traffic spikes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start Embedding Today
&lt;/h2&gt;

&lt;p&gt;Visit any dashboard on FXMacroData and click “Embed Graph” to generate your snippet instantly.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Creating a python SDK for FXMacroData</title>
      <dc:creator>Robert Tidball</dc:creator>
      <pubDate>Tue, 25 Nov 2025 13:46:19 +0000</pubDate>
      <link>https://forem.com/roberttidball/-2g4b</link>
      <guid>https://forem.com/roberttidball/-2g4b</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/fxmacrodata" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__org__pic"&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F11901%2Fbe16b3bd-be49-43fe-9789-919ea63acc35.png" alt="FXMacroData" width="800" height="800"&gt;
      &lt;div class="ltag__link__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3608997%2F531d13ca-ef6b-4bba-8ee2-56cac876c320.jpeg" alt="" width="460" height="460"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://dev.to/fxmacrodata/building-an-fx-trading-edge-creating-a-python-client-for-the-fxmacrodata-api-4nnc" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;🐍 Building an FX Trading Edge: Creating a Python Client for the FXMacroData API&lt;/h2&gt;
      &lt;h3&gt;Robert Tidball for FXMacroData ・ Nov 25&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#python&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#api&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#github&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#programming&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
      <category>python</category>
      <category>api</category>
      <category>github</category>
      <category>programming</category>
    </item>
    <item>
      <title>🐍 Building an FX Trading Edge: Creating a Python Client for the FXMacroData API</title>
      <dc:creator>Robert Tidball</dc:creator>
      <pubDate>Tue, 25 Nov 2025 13:45:20 +0000</pubDate>
      <link>https://forem.com/fxmacrodata/building-an-fx-trading-edge-creating-a-python-client-for-the-fxmacrodata-api-4nnc</link>
      <guid>https://forem.com/fxmacrodata/building-an-fx-trading-edge-creating-a-python-client-for-the-fxmacrodata-api-4nnc</guid>
      <description>&lt;p&gt;When building a Python library, the goal is to turn a complex, boilerplate-heavy process (raw API calls) into a simple, elegant one-liner. The &lt;strong&gt;&lt;a href="https://fxmacrodata.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=fxmacrodata" rel="noopener noreferrer"&gt;FXMacroData&lt;/a&gt;&lt;/strong&gt; API provides real-time macroeconomic indicators for major currency pairs—a goldmine for quant traders and analysts.&lt;/p&gt;

&lt;p&gt;Raw API calls force developers to repeat code for authentication, error checking, and URL construction. Embracing the &lt;strong&gt;DRY (Don't Repeat Yourself)&lt;/strong&gt; principle, I set out to build a dedicated Python library on top of it, creating a user-friendly wrapper. This article walks you through the core components of that wrapper, covering synchronous and asynchronous clients, proper exception handling, and utility functions.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Project Scaffolding and Handling Authentication
&lt;/h2&gt;

&lt;p&gt;A good library starts with an intuitive entry point. My goal was to turn an HTTP request into a clean Python method call like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aud&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inflation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Client Constructor
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Client&lt;/code&gt; class holds the base URL and API key. The FXMacroData API has a unique feature: &lt;strong&gt;USD data is public, but other currencies require an API key.&lt;/strong&gt; The constructor handles this requirement upfront.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# client.py or async_client.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FXMacroDataError&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://fxmacrodata.com/api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        Synchronous FXMacroData Client.
        api_key: Required for non-USD currencies. USD is public.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Core Logic: The Synchronous Client (&lt;code&gt;Client&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;synchronous &lt;code&gt;Client&lt;/code&gt;&lt;/strong&gt; uses the popular &lt;code&gt;requests&lt;/code&gt; library. The main logic resides in the &lt;code&gt;get&lt;/code&gt; method, which dynamically constructs the URL and enforces the API key requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;get&lt;/code&gt; Method: Dynamic URL Construction and Key Check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# client.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;indicator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;indicator&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Custom exception is crucial for user-friendly errors
&lt;/span&gt;            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;FXMacroDataError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API key required for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; endpoints.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;

    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="c1"&gt;# ... params and API call logic ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Robust Error Handling with Custom Exceptions
&lt;/h3&gt;

&lt;p&gt;A robust library must handle failures gracefully. I created a custom exception, &lt;code&gt;FXMacroDataError&lt;/code&gt;, to catch network issues and non-200 status codes, returning a clear, actionable message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# exceptions.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FXMacroDataError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Custom exception for FXMacroData client errors.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Core request logic with the error wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# client.py (continued)
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;FXMacroDataError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Request failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Raise a clear error if the API returns a problem
&lt;/span&gt;    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;FXMacroDataError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API Error (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Advanced Feature: The Asynchronous Client (&lt;code&gt;AsyncClient&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;For automated trading bots or high-traffic dashboards, &lt;strong&gt;asynchronous programming&lt;/strong&gt; is essential for performance. The &lt;code&gt;AsyncClient&lt;/code&gt; uses the &lt;strong&gt;&lt;code&gt;aiohttp&lt;/code&gt;&lt;/strong&gt; library for non-blocking I/O.&lt;/p&gt;

&lt;h3&gt;
  
  
  Asynchronous Session Management
&lt;/h3&gt;

&lt;p&gt;I implemented async context managers (&lt;code&gt;__aenter__&lt;/code&gt; and &lt;code&gt;__aexit__&lt;/code&gt;) to ensure the &lt;code&gt;aiohttp.ClientSession&lt;/code&gt; is created and properly closed, preventing resource leaks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# async_client.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;
&lt;span class="c1"&gt;# ... imports ...
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ... init ...
&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__aenter__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AsyncClient&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__aexit__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_tb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows concurrent execution, where the total time is the maximum delay, not the sum:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Concurrent calls are now trivial
&lt;/span&gt;        &lt;span class="n"&gt;data_aud&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aud&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inflation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;data_eur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eur&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gdp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;aud_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eur_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_aud&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_eur&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Utility: Cleaning Up the Data
&lt;/h2&gt;

&lt;p&gt;Data consumers expect chronologically sorted data, but APIs don't always guarantee it. A small utility function ensures the output is always clean time-series data, checking for either a &lt;code&gt;'date'&lt;/code&gt; or &lt;code&gt;'release_date'&lt;/code&gt; key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# utils.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sort_by_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Sorts a list of indicator data dictionaries by &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; or &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;release_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;release_date&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Building this wrapper solidified my understanding of &lt;strong&gt;Object-Oriented Design&lt;/strong&gt;, the crucial performance trade-offs between &lt;strong&gt;Synchronous vs. Asynchronous&lt;/strong&gt; networking, and the importance of a great &lt;strong&gt;Developer Experience&lt;/strong&gt; through custom exceptions. You can explore the full source code on &lt;a href="https://github.com/fxmacrodata/fxmacrodata" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The final step was packaging the library and publishing it to PyPI. If you're building a tool to integrate real-time FX macro data, or just want a pattern for creating your own wrapper, the structure of this library is a solid foundation. Happy coding!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;— Rob @ &lt;a href="https://fxmacrodata.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=fxmacrodata" rel="noopener noreferrer"&gt;FXMacroData&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>github</category>
      <category>programming</category>
    </item>
    <item>
      <title>How SuburbStory Uses Asyncio to Pull Multi-Source Government Data in Parallel</title>
      <dc:creator>Robert Tidball</dc:creator>
      <pubDate>Mon, 24 Nov 2025 07:16:16 +0000</pubDate>
      <link>https://forem.com/roberttidball/how-suburbstory-uses-asyncio-to-pull-multi-source-government-data-in-parallel-28d8</link>
      <guid>https://forem.com/roberttidball/how-suburbstory-uses-asyncio-to-pull-multi-source-government-data-in-parallel-28d8</guid>
      <description>&lt;p&gt;&lt;a href="https://suburbstory.com/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=asyncio_multi_source_data" rel="noopener noreferrer"&gt;SuburbStory&lt;/a&gt; publishes local updates for every suburb in NSW. The data behind each update comes from several government and public feeds: police, fire, traffic, transport, and various agency alerts. Each of these sources is independent, updated on its own schedule, and exposed through completely different HTTP endpoints.&lt;/p&gt;

&lt;p&gt;Because of that variety, the core engineering problem isn’t processing the data. It’s getting it fast enough, from all sources, without creating a slow and fragile chain of sequential API calls. The entire workload is I/O: pulling remote webpages, parsing lightweight text, and sending the processed information to Gemini to generate suburb-level summaries.&lt;/p&gt;

&lt;p&gt;The only way to make this practical at state-wide scale is to run everything in parallel. That’s where Python’s asyncio becomes the centre of the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Sequential Fetching
&lt;/h2&gt;

&lt;p&gt;If you fetch each source one at a time, you create an artificial bottleneck. A single slow feed (for example a public incident feed that occasionally takes a few seconds to respond) blocks all other data from being processed. When you multiply that across hundreds of suburbs, you lose freshness and your cron job runtime becomes unpredictable.&lt;/p&gt;

&lt;p&gt;But none of these tasks depend on each other. Each fetch is just an isolated HTTP call. There’s no reason to wait for one to finish before starting the next.&lt;/p&gt;

&lt;p&gt;Using asyncio.gather to Hit Every Source at Once&lt;br&gt;
The actual workflow is simple: as soon as the cron job starts, every data source is queried in parallel. Instead of a loop like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for source in sources:
    data = fetch(source)
    process(data)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the pipeline builds a list of coroutines and launches them simultaneously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;results = await asyncio.gather(*tasks, return_exceptions=True)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every request goes out at the same time. Each one resolves whenever the remote server responds. A slow feed no longer slows down anything else. A fast feed is processed immediately. When you have tens of government sources multiplied across thousands of suburbs, this difference is enormous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing and Grouping on the Fly
&lt;/h2&gt;

&lt;p&gt;Once the fetches complete, the content is parsed and normalised. These steps are lightweight — mostly HTML extraction, field cleaning, text slicing, and location matching — so they run inline without blocking the loop. By the time all tasks in the gather call have resolved, you already have a full snapshot of every government update that matters for that cycle.&lt;/p&gt;

&lt;p&gt;The next step is grouping by suburb. Because all sources arrive together, the system can assemble a complete picture of what happened in a suburb across police, fire, traffic, and other feeds in a single pass. That grouped data becomes the input to the NLG stage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel Model Requests to Gemini
&lt;/h2&gt;

&lt;p&gt;Model calls are also I/O. They involve sending the suburb-level dataset to Gemini and waiting for a generated summary. Running these sequentially would be even slower than sequential scraping. But they fit the same pattern as the fetches: independent, high-latency network operations.&lt;/p&gt;

&lt;p&gt;So the pipeline builds another batch of tasks (one model call per suburb) and fires them all at once using asyncio.gather, with concurrency limits where needed. While the model is working on one suburb, dozens of other tasks are in flight. No time is spent waiting for NLG work to complete before starting the next one.&lt;/p&gt;

&lt;p&gt;This effectively compresses what would be minutes of serialized model calls down to the latency of a single batch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The entire cycle looks like this:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;All data sources fetched concurrently&lt;/li&gt;
&lt;li&gt;All parsing done immediately after responses arrive&lt;/li&gt;
&lt;li&gt;All suburb summaries generated concurrently&lt;/li&gt;
&lt;li&gt;All output stored without blocking the loop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cron job doesn’t wait on anything except the slowest item in each batch. Everything else is overlapped. Even though the system touches many external endpoints and calls an LLM at scale, the total runtime stays short and predictable because every part of the pipeline runs in parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Asyncio Suits This Problem Exactly
&lt;/h2&gt;

&lt;p&gt;The workload behind &lt;a href="https://suburbstory.com/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=asyncio_multi_source_data" rel="noopener noreferrer"&gt;SuburbStory&lt;/a&gt; isn’t CPU-heavy. There’s no local ML model, no heavy processing, no long transformations. It’s almost entirely network-bound. Asyncio is designed for this exact shape of problem: many independent I/O operations that can safely run together.&lt;/p&gt;

&lt;p&gt;Using threads or processes would add overhead and resource waste. Using synchronous requests would stretch each cron cycle unnecessarily. Asyncio sits neatly in the middle with minimal overhead, maximum concurrency, and predictable performance.&lt;/p&gt;

</description>
      <category>python</category>
      <category>asyncio</category>
      <category>ai</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Flexible Flask Endpoints for Hierarchical Local News on SuburbStory</title>
      <dc:creator>Robert Tidball</dc:creator>
      <pubDate>Mon, 17 Nov 2025 03:40:03 +0000</pubDate>
      <link>https://forem.com/roberttidball/flexible-flask-endpoints-for-hierarchical-local-news-on-suburbstory-3dd8</link>
      <guid>https://forem.com/roberttidball/flexible-flask-endpoints-for-hierarchical-local-news-on-suburbstory-3dd8</guid>
      <description>&lt;p&gt;&lt;a href="https://suburbstory.com/?utm_source=hashnode&amp;amp;utm_medium=article&amp;amp;utm_campaign=engineering" rel="noopener noreferrer"&gt;SuburbStory&lt;/a&gt; publishes automated local news pages for every suburb in NSW (Australia). The site’s URLs follow a clear geographic hierarchy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://suburbstory.com/&amp;lt;country&amp;gt;/&amp;lt;state&amp;gt;/&amp;lt;area&amp;gt;/&amp;lt;suburb&amp;gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each page is built by aggregating structured data from multiple government sources such as police, fire, traffic, and council feeds. This data is then processed via the Gemini API to generate readable content.&lt;/p&gt;

&lt;p&gt;A key challenge is &lt;strong&gt;efficiently mapping thousands of geographic URL combinations to a single Flask application&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dynamic Routing in Flask
&lt;/h2&gt;

&lt;p&gt;Rather than creating a route for each suburb, SuburbStory uses a single, flexible Flask route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&amp;lt;country&amp;gt;/&amp;lt;state&amp;gt;/&amp;lt;area&amp;gt;/&amp;lt;suburb&amp;gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;suburb_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;area&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;suburb&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;area&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;suburb&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_suburb_doc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# fetch data from Firestore
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;render_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;suburb.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flask automatically parses the URL segments and passes them as function arguments. The handler then retrieves the corresponding Firestore document and renders it with a single template. This allows one route to serve thousands of unique pages across different areas and suburbs without modifying the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real Examples
&lt;/h3&gt;

&lt;p&gt;Two suburbs under the same area illustrate the approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://suburbstory.com/au/nsw/willoughby/chatswood/" rel="noopener noreferrer"&gt;https://suburbstory.com/au/nsw/willoughby/chatswood/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://suburbstory.com/au/nsw/willoughby/artarmon/" rel="noopener noreferrer"&gt;https://suburbstory.com/au/nsw/willoughby/artarmon/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These pages use the same route and template while aggregating their own data, demonstrating how hierarchical Flask endpoints efficiently handle multiple suburbs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Hierarchical URLs Matter
&lt;/h2&gt;

&lt;p&gt;The hierarchical pattern &lt;code&gt;country/state/area/suburb&lt;/code&gt; has several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Predictable and SEO-friendly URLs&lt;/strong&gt; – easy to link externally and for search engines to index.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct mapping to Firestore&lt;/strong&gt; – each page can be stored under a document key like &lt;code&gt;"au/nsw/willoughby/chatswood"&lt;/code&gt;, simplifying retrieval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Template reusability&lt;/strong&gt; – the same &lt;code&gt;suburb.html&lt;/code&gt; template dynamically adjusts based on the URL variables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalable architecture&lt;/strong&gt; – Cloud Run handles all routes in one Flask app, allowing horizontal scaling without additional routing configuration.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Takeaways for Python Developers
&lt;/h2&gt;

&lt;p&gt;SuburbStory demonstrates how flexible Flask routing enables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Serving thousands of hierarchical pages from a single route&lt;/li&gt;
&lt;li&gt;Efficiently handling independent data fetches per page&lt;/li&gt;
&lt;li&gt;Scaling a serverless Flask app on Cloud Run without duplicating code&lt;/li&gt;
&lt;li&gt;Maintaining clean, predictable, SEO-friendly URLs that map directly to Firestore&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a practical model for building large-scale local or multi-tenant content systems in Python.&lt;/p&gt;

</description>
      <category>python</category>
      <category>flask</category>
      <category>google</category>
      <category>cloud</category>
    </item>
    <item>
      <title>🚀 Scaling Up: Why I Chose FastAPI Over Flask and Django for a Data API</title>
      <dc:creator>Robert Tidball</dc:creator>
      <pubDate>Thu, 13 Nov 2025 04:23:02 +0000</pubDate>
      <link>https://forem.com/roberttidball/scaling-up-why-i-chose-fastapi-over-flask-and-django-for-a-data-api-1phi</link>
      <guid>https://forem.com/roberttidball/scaling-up-why-i-chose-fastapi-over-flask-and-django-for-a-data-api-1phi</guid>
      <description>&lt;p&gt;As a developer, when you set out to build a new data service—like my recent project, &lt;strong&gt;FXMacroData&lt;/strong&gt; (check out the live API at &lt;a href="https://fxmacrodata.com/?utm_source=devto&amp;amp;utm_medium=blogpost&amp;amp;utm_campaign=fastapi_choice" rel="noopener noreferrer"&gt;FXMacroData - Real-time Forex Data&lt;/a&gt;) the framework choice is crucial. My goal was simple: serve high-frequency macroeconomic data instantly and reliably.&lt;/p&gt;

&lt;p&gt;I narrowed the field down to the Python giants: &lt;strong&gt;Flask&lt;/strong&gt;, &lt;strong&gt;Django&lt;/strong&gt;, and &lt;strong&gt;FastAPI&lt;/strong&gt;. While Flask is famously lightweight and Django is the enterprise powerhouse, I ultimately landed on &lt;strong&gt;FastAPI&lt;/strong&gt;. Here's why that decision was a game-changer for building a performant, modern data API designed for the cloud.&lt;/p&gt;




&lt;h3&gt;
  
  
  The API Mandate: Speed, Concurrency, and Serverless
&lt;/h3&gt;

&lt;p&gt;The core requirement for the FXMacroData API is high &lt;strong&gt;concurrency&lt;/strong&gt; and an architecture optimized for modern cloud deployments, specifically &lt;strong&gt;serverless&lt;/strong&gt; environments like &lt;strong&gt;Google Cloud Run&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flask (Sync):&lt;/strong&gt; Standard Flask is synchronous (WSGI), meaning it blocks a worker thread while waiting for I/O (like a database query). This inefficiency makes it harder to scale cost-effectively in a serverless environment where every millisecond of CPU time matters.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Django (Monolithic):&lt;/strong&gt; Django is excellent, but it’s a &lt;strong&gt;heavyweight framework&lt;/strong&gt;. For a pure API backend, its many built-in components were simply &lt;strong&gt;overkill&lt;/strong&gt;. Deploying a massive framework just to serve a few data endpoints is inefficient, especially when using a flexible NoSQL backend like &lt;strong&gt;Firestore&lt;/strong&gt;. Its complexity and synchronous default were hurdles to fast, cost-effective serverless deployment.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ⚡️ FastAPI: Natively Async for Cloud Run
&lt;/h3&gt;

&lt;p&gt;FastAPI is built on the modern &lt;strong&gt;ASGI standard&lt;/strong&gt;, making it asynchronous (&lt;strong&gt;async/await&lt;/strong&gt;) from the ground up. This was the single biggest performance advantage:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Non-Blocking I/O:&lt;/strong&gt; Most API operations are I/O-bound (waiting for the database or network). Because FastAPI's worker doesn't block, it can efficiently handle hundreds of concurrent requests using minimal resources. This is essential for &lt;strong&gt;Cloud Run's scaling model&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serverless Integration:&lt;/strong&gt; Being lightweight and ASGI-native means FastAPI runs perfectly within the brief lifespan of a serverless container. It integrates cleanly with external services like &lt;strong&gt;Firestore&lt;/strong&gt;, making it the ideal choice for a &lt;strong&gt;stateless, instant-scaling microservice&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  The Code Difference: I/O Concurrency
&lt;/h3&gt;

&lt;p&gt;The benefit of native asynchronous programming is immediately clear when requesting data from multiple sources.&lt;/p&gt;

&lt;h4&gt;
  
  
  ➡️ Flask (Synchronous/Blocking)
&lt;/h4&gt;

&lt;p&gt;The total execution time is the &lt;strong&gt;sum&lt;/strong&gt; of the two delays (approx. 2 seconds), as the second call must wait for the first to complete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Flask (Synchronous)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Execution runs sequentially: A waits for B to finish.
&lt;/span&gt;&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sync_example&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Wait for Source A
&lt;/span&gt;    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Wait for Source B
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total Time: ~2.0s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  ➡️ FastAPI (Asynchronous/Non-Blocking)
&lt;/h4&gt;

&lt;p&gt;The total execution time is the &lt;strong&gt;maximum&lt;/strong&gt; of the two delays (approx. 1 second), as both I/O operations are initiated concurrently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# FastAPI (Asynchronous)
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Execution runs concurrently: A and B start at the same time.
&lt;/span&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;async_example&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# Wait for Source A
&lt;/span&gt;        &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# Wait for Source B
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total Time: ~1.0s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  🧠 The Productivity Bonus: Clean Code and Auto-Docs
&lt;/h3&gt;

&lt;p&gt;Beyond performance and cloud architecture, FastAPI delivered on developer experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It uses &lt;strong&gt;Pydantic models&lt;/strong&gt; and &lt;strong&gt;Python type hints&lt;/strong&gt; for automatic data validation and serialization.
&lt;/li&gt;
&lt;li&gt;It generates &lt;strong&gt;interactive OpenAPI documentation (Swagger UI)&lt;/strong&gt; automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ultimately, FastAPI allowed me to build a high-performance, stateless API that perfectly matches the pay-per-use efficiency of Google Cloud Run, making it technically superior and far more cost-effective than using an over-engineered framework like Django for this specific task.&lt;/p&gt;

&lt;p&gt;If you're building a new API focused on speed, data movement, and cloud-native scaling, the choice is clear: &lt;strong&gt;go async with FastAPI.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>fastapi</category>
      <category>gcp</category>
    </item>
  </channel>
</rss>
